Tux the Penguipedia
tuxthepenguinwiki
https://tuxthepenguin.miraheze.org/wiki/Main_Page
MediaWiki 1.40.1
first-letter
Media
Special
Talk
User
User talk
Tux the Penguipedia
Tux the Penguipedia talk
File
File talk
MediaWiki
MediaWiki talk
Template
Template talk
Help
Help talk
Category
Category talk
Form
Form talk
Video
Video talk
Campaign
Campaign talk
User blog
User blog talk
TimedText
TimedText talk
Module
Module talk
Gadget
Gadget talk
Gadget definition
Gadget definition talk
Topic
Navigation
Navigation talk
Module:Reply to
828
88
168
2020-11-17T18:27:57Z
dev>BrandonWM
0
Created page with "local p = {} local function makeError(msg) msg ='Error in [[Template:Reply to]]: ' .. msg return mw.text.tag('strong', {['class']='error'}, msg) end function p.replyto(fra..."
Scribunto
text/plain
local p = {}
local function makeError(msg)
msg ='Error in [[Template:Reply to]]: ' .. msg
return mw.text.tag('strong', {['class']='error'}, msg)
end
function p.replyto(frame)
local origArgs = frame:getParent().args
local args = {}
local maxArg = 1
local usernames = 0
for k, v in pairs(origArgs) do
if type(k) == 'number' then
if mw.ustring.match(v,'%S') then
if k > maxArg then maxArg = k end
usernames = usernames + 1
local title = mw.title.new(v)
if not title then return makeError('Input contains forbidden characters.') end
args[k] = title.rootText
end
elseif v == '' and k:sub(0,5) == 'label' then
args[k] = '​'
else
args[k] = v
end
end
if usernames > (tonumber(frame.args.max) or 50) then
return makeError(string.format(
'More than %s names specified.',
tostring(frame.args.max or 50)
))
else
if usernames < 1 then
if frame.args.example then args[1] = frame.args.example else return makeError('Username not given.') end
end
args['label1'] = args['label1'] or args['label']
local isfirst = true
local outStr = args['prefix'] or '@'
for i = 1, maxArg do
if args[i] then
if isfirst then
isfirst = false
else
if ( (usernames > 2) or ((usernames == 2) and (args['c'] == '')) ) then outStr = outStr..', ' end
if i == maxArg then outStr = outStr..' '..(args['c'] or 'and') .. ' ' end
end
outStr = string.format(
'%s[[User:%s|%s]]',
outStr,
args[i],
args['label'..tostring(i)] or args[i]
)
end
end
outStr = outStr..(args['p'] or ':')
return mw.text.tag('span', {['class']='template-ping'}, outStr)
end
end
return p
14f0cd73a8a9f122c0e0e15382219083c602c62a
Template:Not done
10
28
49
2020-11-17T19:23:53Z
dev>MacFan4000
0
9 revisions imported from [[:templatewiki:Template:Not_done]]
wikitext
text/x-wiki
[[File:X mark.svg|18px]] '''{{{1|Not done}}}'''<noinclude>
{{documentation}}
[[Category:Resolution templates]]
</noinclude>
fd025e245bee74ddd6c5ae757f983a3cd3258d94
Template:Doing
10
26
45
2020-11-17T19:24:11Z
dev>MacFan4000
0
46 revisions imported from [[:templatewiki:Template:Doing]]
wikitext
text/x-wiki
[[File:Pictogram voting wait.svg|18px|link=|alt=]] '''{{{1|Doing…}}}'''<noinclude>
{{documentation}}
[[Category:Resolution templates]]
</noinclude>
eb1feacb3d7a14829c6a9be311732ee0d24d35f3
Template:Clear
10
36
64
2022-03-08T14:12:32Z
dev>DarkMatterMan4500
0
Created page with "<div style="clear:{{{1|both}}};"></div><noinclude> {{documentation}} </noinclude>"
wikitext
text/x-wiki
<div style="clear:{{{1|both}}};"></div><noinclude>
{{documentation}}
</noinclude>
38bab3e3d7fbd3d6800d46556e60bc6bac494d72
Template:Talk quote inline
10
54
100
2022-09-30T00:21:50Z
dev>Pppery
0
Oops
wikitext
text/x-wiki
<templatestyles src="Talk quote inline/styles.css" /><!--
--><q {{#if: {{{title|}}} | title="{{{title}}}"}} class="inline-quote-talk {{#if: {{{i|{{{italic|}}}}}} | inline-quote-talk-italic}} {{#if: {{{q|{{{quotes|}}}}}}|inline-quote-talk-marks}}">{{{1|Example text}}}</q><!--
--><noinclude>
{{Documentation}}
</noinclude>
b18e2fdc57277adbf4e3f4f513e78ecc5831453f
Template:Comment
10
25
43
2022-09-30T00:50:50Z
dev>Pppery
0
9 revisions imported from [[:meta:Template:Comment]]
wikitext
text/x-wiki
[[File:Pictogram voting comment.svg|18px|link=]] '''{{{1|Comment:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude>
5f48a21f6ec3dc6d82cfa7a668c9e00fec175396
Template:Information
10
52
96
2022-09-30T01:03:49Z
dev>Pppery
0
wikitext
text/x-wiki
<templatestyles src="Information/style.css" />
<div class="hproduct commons-file-information-table">
<table class="toccolours vevent fileinfotpl-type-information" style="width: 100%;" cellpadding="4">
<!-- Description -->
<tr style="vertical-align: top">
<td id="fileinfotpl_desc" class="fileinfo-paramfield">Description<span class="summary fn" style="display:none">{{PAGENAME}}</span></td>
<td class="description">{{ #if: {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{Description missing}} }}</td>
</tr>
<!-- Source -->
<tr style="vertical-align: top">
<td id="fileinfotpl_src" class="fileinfo-paramfield">Source</td>
<td>{{ #if: {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{Description missing|source information}} }}</td>
</tr>
<!-- Author -->
<tr style="vertical-align: top">
<td id="fileinfotpl_aut" class="fileinfo-paramfield">Author</td>
<td>{{ #if: {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{Description missing|author information}} }}</td>
</tr>
<!-- Fecha -->
<tr style="vertical-align: top">
<td id="fileinfotpl_aut" class="fileinfo-paramfield">Date</td>
<td>{{{date|{{{Date|{{{fecha|{{{Fecha|}}}}}}}}}}}}</td>
</tr>
<!-- Other versions -->
{{#switch: {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}{{{demo|<noinclude>1</noinclude>}}}
| =
| - =
| none =
| #default =
<tr style="vertical-align: top">
<td id="fileinfotpl_ver" class="fileinfo-paramfield" style="background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight:bold">Other versions</td>
<td>
{{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}
</td>
</tr>
}}
</table>
</div><noinclude>{{Documentation}}</noinclude>
3a749131ebeff40d99673728a045f76f81456cc1
Template:On hold
10
37
66
2022-09-30T01:06:11Z
dev>Pppery
0
Pppery moved page [[Template:On Hold]] to [[Template:On hold]]
wikitext
text/x-wiki
[[File:Symbol wait.svg|18px]] '''{{{1|On hold}}}'''<noinclude>[[Category:Resolution templates]]<noinclude>
{{documentation}}
[[Category:Resolution templates]]</noinclude>
7a18e8aa8c80a33b1a68eed60d0993a75202162f
Template:Template link
10
74
140
2022-09-30T01:10:00Z
dev>Pppery
0
46 revisions imported from [[:wikipedia:Template:Template_link]]
wikitext
text/x-wiki
{{[[Template:{{{1}}}|{{{1}}}]]}}<noinclude>{{documentation}}
<!-- Categories go on the /doc subpage and interwikis go on Wikidata. -->
</noinclude>
eabbec62efe3044a98ebb3ce9e7d4d43c222351d
Template:Abstain
10
60
112
2022-09-30T01:41:43Z
dev>Pppery
0
Add doc
wikitext
text/x-wiki
[[File:Symbol neutral vote.svg|18px]] '''<bdi>{{{1|Abstain}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
b1098b70832376165658562bfc8de5b6187bdb26
Template:Documentation
10
18
29
2022-09-30T01:43:37Z
dev>MacFan4000
0
4 revisions imported from [[:meta:Template:Documentation]]: this is useful and was on templateiwki
wikitext
text/x-wiki
{{#invoke:documentation|main|_content={{ {{#invoke:documentation|contentTitle}}}}}}<noinclude>[[Category:Templates]]</noinclude>
9885bb4fa99bf3d5b960e73606bbb8eed3026877
Template:User wikimedia
10
67
126
2022-09-30T01:44:10Z
dev>MacFan4000
0
9 revisions imported from [[:meta:Template:User_wikimedia]]: this is useful and was on templateiwki
wikitext
text/x-wiki
{{Userbox
| id = [[File:Wikimedia Foundation Logo.png|43px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an [[metawikimedia:Special:CentralAuth/{{{account|{{BASEPAGENAME}}}}}|account]] at the Wikimedia Foundation projects.
| nocat = {{{nocat|}}}
| usercategory = Wikimedians
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
c7b06f2b4d088ee94d893eb1e3548e9e0562fc5e
Template:Neutral
10
61
114
2022-09-30T01:53:01Z
dev>Pppery
0
Add doc
wikitext
text/x-wiki
[[File:Symbol neutral vote.svg|18px]] '''<bdi>{{{1|Neutral}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
59552c46cb01ccf2c6196bdea9ec3eb90858e675
Template:Oppose
10
62
116
2022-09-30T01:54:33Z
dev>Pppery
0
Add doc
wikitext
text/x-wiki
{{ #switch: {{{4|{{{1|}}}}}}
| Regular= [[File:Symbol oppose vote.png|18px|alt=]]
| Normal= [[File:Symbol oppose vote.png|18px|alt=]]
| Strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| Strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| Strongest = [[File:Symbol full oppose vote.svg|20px|alt=]]
| Weak= [[File:Weak Oppose.png|18px|alt=]]
| Weakly= [[File:Weak Oppose.png|18px|alt=]]
| strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| weak= [[File:Weak Oppose.png|18px|alt=]]
| weakly= [[File:Weak Oppose.png|18px|alt=]]
| strongest = [[File:Symbol full oppose vote.svg|20px|alt=]]
|#default= [[File:Symbol oppose vote.svg|18px|alt=]]
}} {{ #switch: {{{1|}}}
| Regular='''Oppose'''
| Normal= '''Oppose'''
| Strongest = '''Strongest oppose'''
| Strongly= '''Strongly oppose'''
| Strong= '''Strong oppose'''
| Weak= '''Weak oppose'''
| Weakly= '''Weakly oppose'''
| strongly= '''Strongly oppose'''
| strong= '''Strong oppose'''
| weak= '''Weak oppose'''
| weakly= '''Weakly oppose'''
| strongest = '''Strongest oppose'''
| {{{other|2}}} = '''{{{3}}}'''
|#default= '''Oppose'''
}}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
8c57115a1c36446a717d2c874b2895b057d1ffc3
Template:Ping
10
59
110
2022-09-30T02:01:54Z
dev>Pppery
0
wikitext
text/x-wiki
{{{{{|safesubst:}}}#invoke:Reply to|replyto|<noinclude>example=Example</noinclude>|max=50}}<noinclude>{{documentation}}</noinclude>
0a7b3547181e17a03ec99855e276688fcc36ce1e
Template:Support
10
63
118
2022-09-30T02:02:35Z
dev>Pppery
0
Add doc
wikitext
text/x-wiki
{{ #switch: {{{4|{{{1|}}}}}}
| Regular= [[File:Symbol support vote.svg|18px|alt=]]
| Normal= [[File:Symbol support vote.svg|18px|alt=]]
| Strongly= [[File:Symbol strong support vote.svg|18px|alt=]]
| Strongest= [[File:Symbol full support vote.svg|22px]]
| Strong= [[File:Symbol strong support vote.svg|18px|alt=]]
| Weak= [[File:Symbol partial support vote.svg|18px|alt=]]
| Weakly= [[File:Symbol partial support vote.svg|18px|alt=]]
| strongly= [[File:Symbol strong support vote.svg|18px|alt=]]
| strong= [[File:Symbol strong support vote.svg|18px|alt=]]
| weak= [[File:Symbol partial support vote.svg|18px|alt=]]
| weakly= [[File:Symbol partial support vote.svg|18px|alt=]]
|#default= [[File:Symbol support vote.svg|18px|alt=]]
}} {{ #switch: {{{1|}}}
| Regular='''Support'''
| Normal= '''Support'''
| Strongly= '''Strongly support'''
| Strong= '''Strong support'''
| Weak= '''Weak support'''
| Weakly= '''Weakly support'''
| strongly= '''Strongly support'''
| Strongest= '''''Strongest support'''''
| strong= '''Strong support'''
| weak= '''Weak support'''
| weakly= '''Weakly support'''
| {{{other|2}}} = '''{{{3}}}'''
|#default= '''Support'''
}}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
0a43c05b804693f20b74446f7e7e6d7ccd10c516
Template:=
10
57
106
2022-09-30T02:18:15Z
dev>Pppery
0
Replaced content with "=<noinclude> {{documentation}} </noinclude>"
wikitext
text/x-wiki
=<noinclude>
{{documentation}}
</noinclude>
44f3105df6073eb65369938814d1551b51611402
Template:User discord
10
66
124
2022-09-30T02:26:18Z
dev>Pppery
0
wikitext
text/x-wiki
{{Userbox
| id = #
| id-s = 24
| id-fc = #5865F2
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:Discord|Discord]]{{#if:{{{account|}}}| as ''{{{account}}}''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use Discord
}}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
19b1d90000718152b9058c16c7c1ba13d7cb2715
Module:Arguments
828
21
35
2022-09-30T02:32:01Z
dev>Pppery
0
24 revisions imported from [[:wikipedia:Module:Arguments]]
Scribunto
text/plain
-- This module provides easy processing of arguments passed to Scribunto from
-- #invoke. It is intended for use by other Lua modules, and should not be
-- called from #invoke directly.
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local arguments = {}
-- Generate four different tidyVal functions, so that we don't have to check the
-- options every time we call it.
local function tidyValDefault(key, val)
if type(val) == 'string' then
val = val:match('^%s*(.-)%s*$')
if val == '' then
return nil
else
return val
end
else
return val
end
end
local function tidyValTrimOnly(key, val)
if type(val) == 'string' then
return val:match('^%s*(.-)%s*$')
else
return val
end
end
local function tidyValRemoveBlanksOnly(key, val)
if type(val) == 'string' then
if val:find('%S') then
return val
else
return nil
end
else
return val
end
end
local function tidyValNoChange(key, val)
return val
end
local function matchesTitle(given, title)
local tp = type( given )
return (tp == 'string' or tp == 'number') and mw.title.new( given ).prefixedText == title
end
local translate_mt = { __index = function(t, k) return k end }
function arguments.getArgs(frame, options)
checkType('getArgs', 1, frame, 'table', true)
checkType('getArgs', 2, options, 'table', true)
frame = frame or {}
options = options or {}
--[[
-- Set up argument translation.
--]]
options.translate = options.translate or {}
if getmetatable(options.translate) == nil then
setmetatable(options.translate, translate_mt)
end
if options.backtranslate == nil then
options.backtranslate = {}
for k,v in pairs(options.translate) do
options.backtranslate[v] = k
end
end
if options.backtranslate and getmetatable(options.backtranslate) == nil then
setmetatable(options.backtranslate, {
__index = function(t, k)
if options.translate[k] ~= k then
return nil
else
return k
end
end
})
end
--[[
-- Get the argument tables. If we were passed a valid frame object, get the
-- frame arguments (fargs) and the parent frame arguments (pargs), depending
-- on the options set and on the parent frame's availability. If we weren't
-- passed a valid frame object, we are being called from another Lua module
-- or from the debug console, so assume that we were passed a table of args
-- directly, and assign it to a new variable (luaArgs).
--]]
local fargs, pargs, luaArgs
if type(frame.args) == 'table' and type(frame.getParent) == 'function' then
if options.wrappers then
--[[
-- The wrappers option makes Module:Arguments look up arguments in
-- either the frame argument table or the parent argument table, but
-- not both. This means that users can use either the #invoke syntax
-- or a wrapper template without the loss of performance associated
-- with looking arguments up in both the frame and the parent frame.
-- Module:Arguments will look up arguments in the parent frame
-- if it finds the parent frame's title in options.wrapper;
-- otherwise it will look up arguments in the frame object passed
-- to getArgs.
--]]
local parent = frame:getParent()
if not parent then
fargs = frame.args
else
local title = parent:getTitle():gsub('/sandbox$', '')
local found = false
if matchesTitle(options.wrappers, title) then
found = true
elseif type(options.wrappers) == 'table' then
for _,v in pairs(options.wrappers) do
if matchesTitle(v, title) then
found = true
break
end
end
end
-- We test for false specifically here so that nil (the default) acts like true.
if found or options.frameOnly == false then
pargs = parent.args
end
if not found or options.parentOnly == false then
fargs = frame.args
end
end
else
-- options.wrapper isn't set, so check the other options.
if not options.parentOnly then
fargs = frame.args
end
if not options.frameOnly then
local parent = frame:getParent()
pargs = parent and parent.args or nil
end
end
if options.parentFirst then
fargs, pargs = pargs, fargs
end
else
luaArgs = frame
end
-- Set the order of precedence of the argument tables. If the variables are
-- nil, nothing will be added to the table, which is how we avoid clashes
-- between the frame/parent args and the Lua args.
local argTables = {fargs}
argTables[#argTables + 1] = pargs
argTables[#argTables + 1] = luaArgs
--[[
-- Generate the tidyVal function. If it has been specified by the user, we
-- use that; if not, we choose one of four functions depending on the
-- options chosen. This is so that we don't have to call the options table
-- every time the function is called.
--]]
local tidyVal = options.valueFunc
if tidyVal then
if type(tidyVal) ~= 'function' then
error(
"bad value assigned to option 'valueFunc'"
.. '(function expected, got '
.. type(tidyVal)
.. ')',
2
)
end
elseif options.trim ~= false then
if options.removeBlanks ~= false then
tidyVal = tidyValDefault
else
tidyVal = tidyValTrimOnly
end
else
if options.removeBlanks ~= false then
tidyVal = tidyValRemoveBlanksOnly
else
tidyVal = tidyValNoChange
end
end
--[[
-- Set up the args, metaArgs and nilArgs tables. args will be the one
-- accessed from functions, and metaArgs will hold the actual arguments. Nil
-- arguments are memoized in nilArgs, and the metatable connects all of them
-- together.
--]]
local args, metaArgs, nilArgs, metatable = {}, {}, {}, {}
setmetatable(args, metatable)
local function mergeArgs(tables)
--[[
-- Accepts multiple tables as input and merges their keys and values
-- into one table. If a value is already present it is not overwritten;
-- tables listed earlier have precedence. We are also memoizing nil
-- values, which can be overwritten if they are 's' (soft).
--]]
for _, t in ipairs(tables) do
for key, val in pairs(t) do
if metaArgs[key] == nil and nilArgs[key] ~= 'h' then
local tidiedVal = tidyVal(key, val)
if tidiedVal == nil then
nilArgs[key] = 's'
else
metaArgs[key] = tidiedVal
end
end
end
end
end
--[[
-- Define metatable behaviour. Arguments are memoized in the metaArgs table,
-- and are only fetched from the argument tables once. Fetching arguments
-- from the argument tables is the most resource-intensive step in this
-- module, so we try and avoid it where possible. For this reason, nil
-- arguments are also memoized, in the nilArgs table. Also, we keep a record
-- in the metatable of when pairs and ipairs have been called, so we do not
-- run pairs and ipairs on the argument tables more than once. We also do
-- not run ipairs on fargs and pargs if pairs has already been run, as all
-- the arguments will already have been copied over.
--]]
metatable.__index = function (t, key)
--[[
-- Fetches an argument when the args table is indexed. First we check
-- to see if the value is memoized, and if not we try and fetch it from
-- the argument tables. When we check memoization, we need to check
-- metaArgs before nilArgs, as both can be non-nil at the same time.
-- If the argument is not present in metaArgs, we also check whether
-- pairs has been run yet. If pairs has already been run, we return nil.
-- This is because all the arguments will have already been copied into
-- metaArgs by the mergeArgs function, meaning that any other arguments
-- must be nil.
--]]
if type(key) == 'string' then
key = options.translate[key]
end
local val = metaArgs[key]
if val ~= nil then
return val
elseif metatable.donePairs or nilArgs[key] then
return nil
end
for _, argTable in ipairs(argTables) do
local argTableVal = tidyVal(key, argTable[key])
if argTableVal ~= nil then
metaArgs[key] = argTableVal
return argTableVal
end
end
nilArgs[key] = 'h'
return nil
end
metatable.__newindex = function (t, key, val)
-- This function is called when a module tries to add a new value to the
-- args table, or tries to change an existing value.
if type(key) == 'string' then
key = options.translate[key]
end
if options.readOnly then
error(
'could not write to argument table key "'
.. tostring(key)
.. '"; the table is read-only',
2
)
elseif options.noOverwrite and args[key] ~= nil then
error(
'could not write to argument table key "'
.. tostring(key)
.. '"; overwriting existing arguments is not permitted',
2
)
elseif val == nil then
--[[
-- If the argument is to be overwritten with nil, we need to erase
-- the value in metaArgs, so that __index, __pairs and __ipairs do
-- not use a previous existing value, if present; and we also need
-- to memoize the nil in nilArgs, so that the value isn't looked
-- up in the argument tables if it is accessed again.
--]]
metaArgs[key] = nil
nilArgs[key] = 'h'
else
metaArgs[key] = val
end
end
local function translatenext(invariant)
local k, v = next(invariant.t, invariant.k)
invariant.k = k
if k == nil then
return nil
elseif type(k) ~= 'string' or not options.backtranslate then
return k, v
else
local backtranslate = options.backtranslate[k]
if backtranslate == nil then
-- Skip this one. This is a tail call, so this won't cause stack overflow
return translatenext(invariant)
else
return backtranslate, v
end
end
end
metatable.__pairs = function ()
-- Called when pairs is run on the args table.
if not metatable.donePairs then
mergeArgs(argTables)
metatable.donePairs = true
end
return translatenext, { t = metaArgs }
end
local function inext(t, i)
-- This uses our __index metamethod
local v = t[i + 1]
if v ~= nil then
return i + 1, v
end
end
metatable.__ipairs = function (t)
-- Called when ipairs is run on the args table.
return inext, t, 0
end
return args
end
return arguments
3134ecce8429b810d445e29eae115e2ae4c36c53
Module:Documentation
828
20
33
2022-09-30T02:36:08Z
dev>Pppery
0
Pppery moved page [[Module:Documentation/2]] to [[Module:Documentation]] without leaving a redirect
Scribunto
text/plain
-- This module implements {{documentation}}.
-- Get required modules.
local getArgs = require('Module:Arguments').getArgs
-- Get the config table.
local cfg = mw.loadData('Module:Documentation/config')
local p = {}
-- Often-used functions.
local ugsub = mw.ustring.gsub
----------------------------------------------------------------------------
-- Helper functions
--
-- These are defined as local functions, but are made available in the p
-- table for testing purposes.
----------------------------------------------------------------------------
local function message(cfgKey, valArray, expectType)
--[[
-- Gets a message from the cfg table and formats it if appropriate.
-- The function raises an error if the value from the cfg table is not
-- of the type expectType. The default type for expectType is 'string'.
-- If the table valArray is present, strings such as $1, $2 etc. in the
-- message are substituted with values from the table keys [1], [2] etc.
-- For example, if the message "foo-message" had the value 'Foo $2 bar $1.',
-- message('foo-message', {'baz', 'qux'}) would return "Foo qux bar baz."
--]]
local msg = cfg[cfgKey]
expectType = expectType or 'string'
if type(msg) ~= expectType then
error('message: type error in message cfg.' .. cfgKey .. ' (' .. expectType .. ' expected, got ' .. type(msg) .. ')', 2)
end
if not valArray then
return msg
end
local function getMessageVal(match)
match = tonumber(match)
return valArray[match] or error('message: no value found for key $' .. match .. ' in message cfg.' .. cfgKey, 4)
end
return ugsub(msg, '$([1-9][0-9]*)', getMessageVal)
end
p.message = message
local function makeWikilink(page, display)
if display then
return mw.ustring.format('[[%s|%s]]', page, display)
else
return mw.ustring.format('[[%s]]', page)
end
end
p.makeWikilink = makeWikilink
local function makeCategoryLink(cat, sort)
local catns = mw.site.namespaces[14].name
return makeWikilink(catns .. ':' .. cat, sort)
end
p.makeCategoryLink = makeCategoryLink
local function makeUrlLink(url, display)
return mw.ustring.format('[%s %s]', url, display)
end
p.makeUrlLink = makeUrlLink
local function makeToolbar(...)
local ret = {}
local lim = select('#', ...)
if lim < 1 then
return nil
end
for i = 1, lim do
ret[#ret + 1] = select(i, ...)
end
-- 'documentation-toolbar'
return '<span class="' .. message('toolbar-class') .. '">('
.. table.concat(ret, ' | ') .. ')</span>'
end
p.makeToolbar = makeToolbar
----------------------------------------------------------------------------
-- Argument processing
----------------------------------------------------------------------------
local function makeInvokeFunc(funcName)
return function (frame)
local args = getArgs(frame, {
valueFunc = function (key, value)
if type(value) == 'string' then
value = value:match('^%s*(.-)%s*$') -- Remove whitespace.
if key == 'heading' or value ~= '' then
return value
else
return nil
end
else
return value
end
end
})
return p[funcName](args)
end
end
----------------------------------------------------------------------------
-- Entry points
----------------------------------------------------------------------------
function p.nonexistent(frame)
if mw.title.getCurrentTitle().subpageText == 'testcases' then
return frame:expandTemplate{title = 'module test cases notice'}
else
return p.main(frame)
end
end
p.main = makeInvokeFunc('_main')
function p._main(args)
--[[
-- This function defines logic flow for the module.
-- @args - table of arguments passed by the user
--]]
local env = p.getEnvironment(args)
local root = mw.html.create()
root
:tag('div')
-- 'documentation-container'
:addClass(message('container'))
:attr('role', 'complementary')
:attr('aria-labelledby', args.heading ~= '' and 'documentation-heading' or nil)
:attr('aria-label', args.heading == '' and 'Documentation' or nil)
:newline()
:tag('div')
-- 'documentation'
:addClass(message('main-div-classes'))
:newline()
:wikitext(p._startBox(args, env))
:wikitext(p._content(args, env))
:tag('div')
-- 'documentation-clear'
:addClass(message('clear'))
:done()
:newline()
:done()
:wikitext(p._endBox(args, env))
:done()
:wikitext(p.addTrackingCategories(env))
-- 'Module:Documentation/styles.css'
return mw.getCurrentFrame():extensionTag (
'templatestyles', '', {src=cfg['templatestyles']
}) .. tostring(root)
end
----------------------------------------------------------------------------
-- Environment settings
----------------------------------------------------------------------------
function p.getEnvironment(args)
--[[
-- Returns a table with information about the environment, including title
-- objects and other namespace- or path-related data.
-- @args - table of arguments passed by the user
--
-- Title objects include:
-- env.title - the page we are making documentation for (usually the current title)
-- env.templateTitle - the template (or module, file, etc.)
-- env.docTitle - the /doc subpage.
-- env.sandboxTitle - the /sandbox subpage.
-- env.testcasesTitle - the /testcases subpage.
--
-- Data includes:
-- env.subjectSpace - the number of the title's subject namespace.
-- env.docSpace - the number of the namespace the title puts its documentation in.
-- env.docpageBase - the text of the base page of the /doc, /sandbox and /testcases pages, with namespace.
-- env.compareUrl - URL of the Special:ComparePages page comparing the sandbox with the template.
--
-- All table lookups are passed through pcall so that errors are caught. If an error occurs, the value
-- returned will be nil.
--]]
local env, envFuncs = {}, {}
-- Set up the metatable. If triggered we call the corresponding function in the envFuncs table. The value
-- returned by that function is memoized in the env table so that we don't call any of the functions
-- more than once. (Nils won't be memoized.)
setmetatable(env, {
__index = function (t, key)
local envFunc = envFuncs[key]
if envFunc then
local success, val = pcall(envFunc)
if success then
env[key] = val -- Memoise the value.
return val
end
end
return nil
end
})
function envFuncs.title()
-- The title object for the current page, or a test page passed with args.page.
local title
local titleArg = args.page
if titleArg then
title = mw.title.new(titleArg)
else
title = mw.title.getCurrentTitle()
end
return title
end
function envFuncs.templateTitle()
--[[
-- The template (or module, etc.) title object.
-- Messages:
-- 'sandbox-subpage' --> 'sandbox'
-- 'testcases-subpage' --> 'testcases'
--]]
local subjectSpace = env.subjectSpace
local title = env.title
local subpage = title.subpageText
if subpage == message('sandbox-subpage') or subpage == message('testcases-subpage') then
return mw.title.makeTitle(subjectSpace, title.baseText)
else
return mw.title.makeTitle(subjectSpace, title.text)
end
end
function envFuncs.docTitle()
--[[
-- Title object of the /doc subpage.
-- Messages:
-- 'doc-subpage' --> 'doc'
--]]
local title = env.title
local docname = args[1] -- User-specified doc page.
local docpage
if docname then
docpage = docname
else
docpage = env.docpageBase .. '/' .. message('doc-subpage')
end
return mw.title.new(docpage)
end
function envFuncs.sandboxTitle()
--[[
-- Title object for the /sandbox subpage.
-- Messages:
-- 'sandbox-subpage' --> 'sandbox'
--]]
return mw.title.new(env.docpageBase .. '/' .. message('sandbox-subpage'))
end
function envFuncs.testcasesTitle()
--[[
-- Title object for the /testcases subpage.
-- Messages:
-- 'testcases-subpage' --> 'testcases'
--]]
return mw.title.new(env.docpageBase .. '/' .. message('testcases-subpage'))
end
function envFuncs.subjectSpace()
-- The subject namespace number.
return mw.site.namespaces[env.title.namespace].subject.id
end
function envFuncs.docSpace()
-- The documentation namespace number. For most namespaces this is the
-- same as the subject namespace. However, pages in the Article, File,
-- MediaWiki or Category namespaces must have their /doc, /sandbox and
-- /testcases pages in talk space.
local subjectSpace = env.subjectSpace
if subjectSpace == 0 or subjectSpace == 6 or subjectSpace == 8 or subjectSpace == 14 then
return subjectSpace + 1
else
return subjectSpace
end
end
function envFuncs.docpageBase()
-- The base page of the /doc, /sandbox, and /testcases subpages.
-- For some namespaces this is the talk page, rather than the template page.
local templateTitle = env.templateTitle
local docSpace = env.docSpace
local docSpaceText = mw.site.namespaces[docSpace].name
-- Assemble the link. docSpace is never the main namespace, so we can hardcode the colon.
return docSpaceText .. ':' .. templateTitle.text
end
function envFuncs.compareUrl()
-- Diff link between the sandbox and the main template using [[Special:ComparePages]].
local templateTitle = env.templateTitle
local sandboxTitle = env.sandboxTitle
if templateTitle.exists and sandboxTitle.exists then
local compareUrl = mw.uri.fullUrl(
'Special:ComparePages',
{ page1 = templateTitle.prefixedText, page2 = sandboxTitle.prefixedText}
)
return tostring(compareUrl)
else
return nil
end
end
return env
end
----------------------------------------------------------------------------
-- Start box
----------------------------------------------------------------------------
p.startBox = makeInvokeFunc('_startBox')
function p._startBox(args, env)
--[[
-- This function generates the start box.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- The actual work is done by p.makeStartBoxLinksData and p.renderStartBoxLinks which make
-- the [view] [edit] [history] [purge] links, and by p.makeStartBoxData and p.renderStartBox
-- which generate the box HTML.
--]]
env = env or p.getEnvironment(args)
local links
local content = args.content
if not content or args[1] then
-- No need to include the links if the documentation is on the template page itself.
local linksData = p.makeStartBoxLinksData(args, env)
if linksData then
links = p.renderStartBoxLinks(linksData)
end
end
-- Generate the start box html.
local data = p.makeStartBoxData(args, env, links)
if data then
return p.renderStartBox(data)
else
-- User specified no heading.
return nil
end
end
function p.makeStartBoxLinksData(args, env)
--[[
-- Does initial processing of data to make the [view] [edit] [history] [purge] links.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'view-link-display' --> 'view'
-- 'edit-link-display' --> 'edit'
-- 'history-link-display' --> 'history'
-- 'purge-link-display' --> 'purge'
-- 'module-preload' --> 'Template:Documentation/preload-module-doc'
-- 'docpage-preload' --> 'Template:Documentation/preload'
-- 'create-link-display' --> 'create'
--]]
local subjectSpace = env.subjectSpace
local title = env.title
local docTitle = env.docTitle
if not title or not docTitle then
return nil
end
if docTitle.isRedirect then
docTitle = docTitle.redirectTarget
end
local data = {}
data.title = title
data.docTitle = docTitle
-- View, display, edit, and purge links if /doc exists.
data.viewLinkDisplay = message('view-link-display')
data.editLinkDisplay = message('edit-link-display')
data.historyLinkDisplay = message('history-link-display')
data.purgeLinkDisplay = message('purge-link-display')
-- Create link if /doc doesn't exist.
local preload = args.preload
if not preload then
if subjectSpace == 828 then -- Module namespace
preload = message('module-preload')
else
preload = message('docpage-preload')
end
end
data.preload = preload
data.createLinkDisplay = message('create-link-display')
return data
end
function p.renderStartBoxLinks(data)
--[[
-- Generates the [view][edit][history][purge] or [create][purge] links from the data table.
-- @data - a table of data generated by p.makeStartBoxLinksData
--]]
local function escapeBrackets(s)
-- Escapes square brackets with HTML entities.
s = s:gsub('%[', '[') -- Replace square brackets with HTML entities.
s = s:gsub('%]', ']')
return s
end
local ret
local docTitle = data.docTitle
local title = data.title
local purgeLink = makeUrlLink(title:fullUrl{action = 'purge'}, data.purgeLinkDisplay)
if docTitle.exists then
local viewLink = makeWikilink(docTitle.prefixedText, data.viewLinkDisplay)
local editLink = makeUrlLink(docTitle:fullUrl{action = 'edit'}, data.editLinkDisplay)
local historyLink = makeUrlLink(docTitle:fullUrl{action = 'history'}, data.historyLinkDisplay)
ret = '[%s] [%s] [%s] [%s]'
ret = escapeBrackets(ret)
ret = mw.ustring.format(ret, viewLink, editLink, historyLink, purgeLink)
else
local createLink = makeUrlLink(docTitle:fullUrl{action = 'edit', preload = data.preload}, data.createLinkDisplay)
ret = '[%s] [%s]'
ret = escapeBrackets(ret)
ret = mw.ustring.format(ret, createLink, purgeLink)
end
return ret
end
function p.makeStartBoxData(args, env, links)
--[=[
-- Does initial processing of data to pass to the start-box render function, p.renderStartBox.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- @links - a string containing the [view][edit][history][purge] links - could be nil if there's an error.
--
-- Messages:
-- 'documentation-icon-wikitext' --> '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]'
-- 'template-namespace-heading' --> 'Template documentation'
-- 'module-namespace-heading' --> 'Module documentation'
-- 'file-namespace-heading' --> 'Summary'
-- 'other-namespaces-heading' --> 'Documentation'
-- 'testcases-create-link-display' --> 'create'
--]=]
local subjectSpace = env.subjectSpace
if not subjectSpace then
-- Default to an "other namespaces" namespace, so that we get at least some output
-- if an error occurs.
subjectSpace = 2
end
local data = {}
-- Heading
local heading = args.heading -- Blank values are not removed.
if heading == '' then
-- Don't display the start box if the heading arg is defined but blank.
return nil
end
if heading then
data.heading = heading
elseif subjectSpace == 10 then -- Template namespace
data.heading = message('documentation-icon-wikitext') .. ' ' .. message('template-namespace-heading')
elseif subjectSpace == 828 then -- Module namespace
data.heading = message('documentation-icon-wikitext') .. ' ' .. message('module-namespace-heading')
elseif subjectSpace == 6 then -- File namespace
data.heading = message('file-namespace-heading')
else
data.heading = message('other-namespaces-heading')
end
-- Heading CSS
local headingStyle = args['heading-style']
if headingStyle then
data.headingStyleText = headingStyle
else
-- 'documentation-heading'
data.headingClass = message('main-div-heading-class')
end
-- Data for the [view][edit][history][purge] or [create] links.
if links then
-- 'mw-editsection-like plainlinks'
data.linksClass = message('start-box-link-classes')
data.links = links
end
return data
end
function p.renderStartBox(data)
-- Renders the start box html.
-- @data - a table of data generated by p.makeStartBoxData.
local sbox = mw.html.create('div')
sbox
-- 'documentation-startbox'
:addClass(message('start-box-class'))
:newline()
:tag('span')
:addClass(data.headingClass)
:attr('id', 'documentation-heading')
:cssText(data.headingStyleText)
:wikitext(data.heading)
local links = data.links
if links then
sbox:tag('span')
:addClass(data.linksClass)
:attr('id', data.linksId)
:wikitext(links)
end
return tostring(sbox)
end
----------------------------------------------------------------------------
-- Documentation content
----------------------------------------------------------------------------
p.content = makeInvokeFunc('_content')
function p._content(args, env)
-- Displays the documentation contents
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
env = env or p.getEnvironment(args)
local docTitle = env.docTitle
local content = args.content
if not content and docTitle and docTitle.exists then
content = args._content or mw.getCurrentFrame():expandTemplate{title = docTitle.prefixedText}
end
-- The line breaks below are necessary so that "=== Headings ===" at the start and end
-- of docs are interpreted correctly.
return '\n' .. (content or '') .. '\n'
end
p.contentTitle = makeInvokeFunc('_contentTitle')
function p._contentTitle(args, env)
env = env or p.getEnvironment(args)
local docTitle = env.docTitle
if not args.content and docTitle and docTitle.exists then
return docTitle.prefixedText
else
return ''
end
end
----------------------------------------------------------------------------
-- End box
----------------------------------------------------------------------------
p.endBox = makeInvokeFunc('_endBox')
function p._endBox(args, env)
--[=[
-- This function generates the end box (also known as the link box).
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
--]=]
-- Get environment data.
env = env or p.getEnvironment(args)
local subjectSpace = env.subjectSpace
local docTitle = env.docTitle
if not subjectSpace or not docTitle then
return nil
end
-- Check whether we should output the end box at all. Add the end
-- box by default if the documentation exists or if we are in the
-- user, module or template namespaces.
local linkBox = args['link box']
if linkBox == 'off'
or not (
docTitle.exists
or subjectSpace == 2
or subjectSpace == 828
or subjectSpace == 10
)
then
return nil
end
-- Assemble the link box.
local text = ''
if linkBox then
text = text .. linkBox
else
text = text .. (p.makeDocPageBlurb(args, env) or '') -- "This documentation is transcluded from [[Foo]]."
if subjectSpace == 2 or subjectSpace == 10 or subjectSpace == 828 then
-- We are in the user, template or module namespaces.
-- Add sandbox and testcases links.
-- "Editors can experiment in this template's sandbox and testcases pages."
text = text .. (p.makeExperimentBlurb(args, env) or '') .. '<br />'
if not args.content and not args[1] then
-- "Please add categories to the /doc subpage."
-- Don't show this message with inline docs or with an explicitly specified doc page,
-- as then it is unclear where to add the categories.
text = text .. (p.makeCategoriesBlurb(args, env) or '')
end
text = text .. ' ' .. (p.makeSubpagesBlurb(args, env) or '') --"Subpages of this template"
end
end
local box = mw.html.create('div')
-- 'documentation-metadata'
box:attr('role', 'note')
:addClass(message('end-box-class'))
-- 'plainlinks'
:addClass(message('end-box-plainlinks'))
:wikitext(text)
:done()
return '\n' .. tostring(box)
end
function p.makeDocPageBlurb(args, env)
--[=[
-- Makes the blurb "This documentation is transcluded from [[Template:Foo]] (edit, history)".
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'edit-link-display' --> 'edit'
-- 'history-link-display' --> 'history'
-- 'transcluded-from-blurb' -->
-- 'The above [[Wikipedia:Template documentation|documentation]]
-- is [[Help:Transclusion|transcluded]] from $1.'
-- 'module-preload' --> 'Template:Documentation/preload-module-doc'
-- 'create-link-display' --> 'create'
-- 'create-module-doc-blurb' -->
-- 'You might want to $1 a documentation page for this [[Wikipedia:Lua|Scribunto module]].'
--]=]
local docTitle = env.docTitle
if not docTitle then
return nil
end
local ret
if docTitle.exists then
-- /doc exists; link to it.
local docLink = makeWikilink(docTitle.prefixedText)
local editUrl = docTitle:fullUrl{action = 'edit'}
local editDisplay = message('edit-link-display')
local editLink = makeUrlLink(editUrl, editDisplay)
local historyUrl = docTitle:fullUrl{action = 'history'}
local historyDisplay = message('history-link-display')
local historyLink = makeUrlLink(historyUrl, historyDisplay)
ret = message('transcluded-from-blurb', {docLink})
.. ' '
.. makeToolbar(editLink, historyLink)
.. '<br />'
elseif env.subjectSpace == 828 then
-- /doc does not exist; ask to create it.
local createUrl = docTitle:fullUrl{action = 'edit', preload = message('module-preload')}
local createDisplay = message('create-link-display')
local createLink = makeUrlLink(createUrl, createDisplay)
ret = message('create-module-doc-blurb', {createLink})
.. '<br />'
end
return ret
end
function p.makeExperimentBlurb(args, env)
--[[
-- Renders the text "Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages."
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'sandbox-link-display' --> 'sandbox'
-- 'sandbox-edit-link-display' --> 'edit'
-- 'compare-link-display' --> 'diff'
-- 'module-sandbox-preload' --> 'Template:Documentation/preload-module-sandbox'
-- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- 'sandbox-create-link-display' --> 'create'
-- 'mirror-edit-summary' --> 'Create sandbox version of $1'
-- 'mirror-link-display' --> 'mirror'
-- 'mirror-link-preload' --> 'Template:Documentation/mirror'
-- 'sandbox-link-display' --> 'sandbox'
-- 'testcases-link-display' --> 'testcases'
-- 'testcases-edit-link-display'--> 'edit'
-- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- 'testcases-create-link-display' --> 'create'
-- 'testcases-link-display' --> 'testcases'
-- 'testcases-edit-link-display' --> 'edit'
-- 'module-testcases-preload' --> 'Template:Documentation/preload-module-testcases'
-- 'template-testcases-preload' --> 'Template:Documentation/preload-testcases'
-- 'experiment-blurb-module' --> 'Editors can experiment in this module's $1 and $2 pages.'
-- 'experiment-blurb-template' --> 'Editors can experiment in this template's $1 and $2 pages.'
--]]
local subjectSpace = env.subjectSpace
local templateTitle = env.templateTitle
local sandboxTitle = env.sandboxTitle
local testcasesTitle = env.testcasesTitle
local templatePage = templateTitle.prefixedText
if not subjectSpace or not templateTitle or not sandboxTitle or not testcasesTitle then
return nil
end
-- Make links.
local sandboxLinks, testcasesLinks
if sandboxTitle.exists then
local sandboxPage = sandboxTitle.prefixedText
local sandboxDisplay = message('sandbox-link-display')
local sandboxLink = makeWikilink(sandboxPage, sandboxDisplay)
local sandboxEditUrl = sandboxTitle:fullUrl{action = 'edit'}
local sandboxEditDisplay = message('sandbox-edit-link-display')
local sandboxEditLink = makeUrlLink(sandboxEditUrl, sandboxEditDisplay)
local compareUrl = env.compareUrl
local compareLink
if compareUrl then
local compareDisplay = message('compare-link-display')
compareLink = makeUrlLink(compareUrl, compareDisplay)
end
sandboxLinks = sandboxLink .. ' ' .. makeToolbar(sandboxEditLink, compareLink)
else
local sandboxPreload
if subjectSpace == 828 then
sandboxPreload = message('module-sandbox-preload')
else
sandboxPreload = message('template-sandbox-preload')
end
local sandboxCreateUrl = sandboxTitle:fullUrl{action = 'edit', preload = sandboxPreload}
local sandboxCreateDisplay = message('sandbox-create-link-display')
local sandboxCreateLink = makeUrlLink(sandboxCreateUrl, sandboxCreateDisplay)
local mirrorSummary = message('mirror-edit-summary', {makeWikilink(templatePage)})
local mirrorPreload = message('mirror-link-preload')
local mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = mirrorPreload, summary = mirrorSummary}
if subjectSpace == 828 then
mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = templateTitle.prefixedText, summary = mirrorSummary}
end
local mirrorDisplay = message('mirror-link-display')
local mirrorLink = makeUrlLink(mirrorUrl, mirrorDisplay)
sandboxLinks = message('sandbox-link-display') .. ' ' .. makeToolbar(sandboxCreateLink, mirrorLink)
end
if testcasesTitle.exists then
local testcasesPage = testcasesTitle.prefixedText
local testcasesDisplay = message('testcases-link-display')
local testcasesLink = makeWikilink(testcasesPage, testcasesDisplay)
local testcasesEditUrl = testcasesTitle:fullUrl{action = 'edit'}
local testcasesEditDisplay = message('testcases-edit-link-display')
local testcasesEditLink = makeUrlLink(testcasesEditUrl, testcasesEditDisplay)
-- for Modules, add testcases run link if exists
if testcasesTitle.contentModel == "Scribunto" and testcasesTitle.talkPageTitle and testcasesTitle.talkPageTitle.exists then
local testcasesRunLinkDisplay = message('testcases-run-link-display')
local testcasesRunLink = makeWikilink(testcasesTitle.talkPageTitle.prefixedText, testcasesRunLinkDisplay)
testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink, testcasesRunLink)
else
testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink)
end
else
local testcasesPreload
if subjectSpace == 828 then
testcasesPreload = message('module-testcases-preload')
else
testcasesPreload = message('template-testcases-preload')
end
local testcasesCreateUrl = testcasesTitle:fullUrl{action = 'edit', preload = testcasesPreload}
local testcasesCreateDisplay = message('testcases-create-link-display')
local testcasesCreateLink = makeUrlLink(testcasesCreateUrl, testcasesCreateDisplay)
testcasesLinks = message('testcases-link-display') .. ' ' .. makeToolbar(testcasesCreateLink)
end
local messageName
if subjectSpace == 828 then
messageName = 'experiment-blurb-module'
else
messageName = 'experiment-blurb-template'
end
return message(messageName, {sandboxLinks, testcasesLinks})
end
function p.makeCategoriesBlurb(args, env)
--[[
-- Generates the text "Please add categories to the /doc subpage."
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'doc-link-display' --> '/doc'
-- 'add-categories-blurb' --> 'Please add categories to the $1 subpage.'
--]]
local docTitle = env.docTitle
if not docTitle then
return nil
end
local docPathLink = makeWikilink(docTitle.prefixedText, message('doc-link-display'))
return message('add-categories-blurb', {docPathLink})
end
function p.makeSubpagesBlurb(args, env)
--[[
-- Generates the "Subpages of this template" link.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'template-pagetype' --> 'template'
-- 'module-pagetype' --> 'module'
-- 'default-pagetype' --> 'page'
-- 'subpages-link-display' --> 'Subpages of this $1'
--]]
local subjectSpace = env.subjectSpace
local templateTitle = env.templateTitle
if not subjectSpace or not templateTitle then
return nil
end
local pagetype
if subjectSpace == 10 then
pagetype = message('template-pagetype')
elseif subjectSpace == 828 then
pagetype = message('module-pagetype')
else
pagetype = message('default-pagetype')
end
local subpagesLink = makeWikilink(
'Special:PrefixIndex/' .. templateTitle.prefixedText .. '/',
message('subpages-link-display', {pagetype})
)
return message('subpages-blurb', {subpagesLink})
end
----------------------------------------------------------------------------
-- Tracking categories
----------------------------------------------------------------------------
function p.addTrackingCategories(env)
--[[
-- Check if {{documentation}} is transcluded on a /doc or /testcases page.
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'display-strange-usage-category' --> true
-- 'doc-subpage' --> 'doc'
-- 'testcases-subpage' --> 'testcases'
-- 'strange-usage-category' --> 'Wikipedia pages with strange ((documentation)) usage'
--
-- /testcases pages in the module namespace are not categorised, as they may have
-- {{documentation}} transcluded automatically.
--]]
local title = env.title
local subjectSpace = env.subjectSpace
if not title or not subjectSpace then
return nil
end
local subpage = title.subpageText
local ret = ''
if message('display-strange-usage-category', nil, 'boolean')
and (
subpage == message('doc-subpage')
or subjectSpace ~= 828 and subpage == message('testcases-subpage')
)
then
ret = ret .. makeCategoryLink(message('strange-usage-category'))
end
return ret
end
return p
78cc3a78f2b5dbb267fa16027c0800a22dbd3c42
Module:Userbox
828
94
180
2022-09-30T13:34:01Z
dev>Pppery
0
Let's not use [[Module:Category handler]] and its dependency web here please
Scribunto
text/plain
-- This module implements {{userbox}}.
local p = {}
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function checkNum(val, default)
-- Checks whether a value is a number greater than or equal to zero. If so,
-- returns it as a number. If not, returns a default value.
val = tonumber(val)
if val and val >= 0 then
return val
else
return default
end
end
local function addSuffix(num, suffix)
-- Turns a number into a string and adds a suffix.
if num then
return tostring(num) .. suffix
else
return nil
end
end
local function checkNumAndAddSuffix(num, default, suffix)
-- Checks a value with checkNum and adds a suffix.
num = checkNum(num, default)
return addSuffix(num, suffix)
end
local function makeCat(cat, sort)
-- Makes a category link.
if sort then
return mw.ustring.format('[[Category:%s|%s]]', cat, sort)
else
return mw.ustring.format('[[Category:%s]]', cat)
end
end
--------------------------------------------------------------------------------
-- Argument processing
--------------------------------------------------------------------------------
local function makeInvokeFunc(funcName)
return function (frame)
local origArgs = require('Module:Arguments').getArgs(frame)
local args = {}
for k, v in pairs(origArgs) do
args[k] = v
end
return p.main(funcName, args)
end
end
p.userbox = makeInvokeFunc('_userbox')
p['userbox-2'] = makeInvokeFunc('_userbox-2')
p['userbox-r'] = makeInvokeFunc('_userbox-r')
--------------------------------------------------------------------------------
-- Main functions
--------------------------------------------------------------------------------
function p.main(funcName, args)
local userboxData = p[funcName](args)
local userbox = p.render(userboxData)
local cats = p.categories(args)
return userbox .. (cats or '')
end
function p._userbox(args)
-- Does argument processing for {{userbox}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee'
-- Get info values.
data.info = args.info or args[4] or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt')
data.infoHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em'
data.infoColor = args['info-color'] or args['info-fc'] or 'black'
data.infoOtherParams = args['info-other-param'] or args['info-op']
data.infoClass = args['info-class']
-- Get id values.
local id = args.logo or args[3] or args.id
data.id = id
data.showId = id and true or false
data.idWidth = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px')
data.idHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.idBackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd'
data.idTextAlign = args['id-a'] or 'center'
data.idFontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt')
data.idColor = args['logo-color'] or args['id-fc'] or data.infoColor
data.idPadding = args['logo-padding'] or args['id-p'] or '0 1px 0 0'
data.idLineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em'
data.idOtherParams = args['logo-other-param'] or args['id-op']
data.idClass = args['id-class']
return data
end
p['_userbox-2'] = function (args)
-- Does argument processing for {{userbox-2}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args[9] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args[1] or args['border-c'] or args['id1-c'] or '#999999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args[2] or args['info-c'] or '#eeeeee'
-- Get info values.
data.info = args[4] or args.info or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-s'], 8, 'pt')
data.infoColor = args[8] or args['info-fc'] or 'black'
data.infoPadding = args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-lh'] or '1.25em'
data.infoOtherParams = args['info-op']
-- Get id values.
data.showId = true
data.id = args.logo or args[3] or args.id1 or 'id1'
data.idWidth = checkNumAndAddSuffix(args['id1-w'], 45, 'px')
data.idHeight = checkNumAndAddSuffix(args['id-h'], 45, 'px')
data.idBackgroundColor = args[1] or args['id1-c'] or '#dddddd'
data.idTextAlign = 'center'
data.idFontSize = checkNumAndAddSuffix(args['id1-s'], 14, 'pt')
data.idLineHeight = args['id1-lh'] or '1.25em'
data.idColor = args['id1-fc'] or data.infoColor
data.idPadding = args['id1-p'] or '0 1px 0 0'
data.idOtherParams = args['id1-op']
-- Get id2 values.
data.showId2 = true
data.id2 = args.logo or args[5] or args.id2 or 'id2'
data.id2Width = checkNumAndAddSuffix(args['id2-w'], 45, 'px')
data.id2Height = data.idHeight
data.id2BackgroundColor = args[7] or args['id2-c'] or args[1] or '#dddddd'
data.id2TextAlign = 'center'
data.id2FontSize = checkNumAndAddSuffix(args['id2-s'], 14, 'pt')
data.id2LineHeight = args['id2-lh'] or '1.25em'
data.id2Color = args['id2-fc'] or data.infoColor
data.id2Padding = args['id2-p'] or '0 0 0 1px'
data.id2OtherParams = args['id2-op']
return data
end
p['_userbox-r'] = function (args)
-- Does argument processing for {{userbox-r}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee'
-- Get id values.
data.showId = false -- We only show id2 in userbox-r.
-- Get info values.
data.info = args.info or args[4] or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-align'] or args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt')
data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em'
data.infoColor = args['info-color'] or args['info-fc'] or 'black'
data.infoOtherParams = args['info-other-param'] or args['info-op']
-- Get id2 values.
data.showId2 = true
data.id2 = args.logo or args[3] or args.id or 'id'
data.id2Width = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px')
data.id2Height = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.id2BackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd'
data.id2TextAlign = args['id-a'] or 'center'
data.id2FontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt')
data.id2Color = args['logo-color'] or args['id-fc'] or data.infoColor
data.id2Padding = args['logo-padding'] or args['id-p'] or '0 0 0 1px'
data.id2LineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em'
data.id2OtherParams = args['logo-other-param'] or args['id-op']
return data
end
function p.render(data)
-- Renders the userbox html using the content of the data table.
-- Render the div tag html.
local root = mw.html.create('div')
root
:css('float', data.float)
:css('border', (data.borderWidth or '') .. ' solid ' .. (data.borderColor or ''))
:css('margin', '1px')
:css('width', data.width)
:addClass('wikipediauserbox')
:addClass(data.bodyClass)
-- Render the table tag html.
local tableroot = root:tag('table')
tableroot
:css('border-collapse', 'collapse')
:css('width', data.width)
:css('margin-bottom', '0')
:css('margin-top', '0')
:css('background', data.backgroundColor)
-- Render the id html.
local tablerow = tableroot:tag('tr')
if data.showId then
tablerow:tag('th')
:css('border', '0')
:css('width', data.idWidth)
:css('height', data.idHeight)
:css('background', data.idBackgroundColor)
:css('text-align', data.idTextAlign)
:css('font-size', data.idFontSize)
:css('color', data.idColor)
:css('padding', data.idPadding)
:css('line-height', data.idLineHeight)
:css('vertical-align', 'middle')
:cssText(data.idOtherParams)
:addClass(data.idClass)
:wikitext(data.id)
end
-- Render the info html.
tablerow:tag('td')
:css('border', '0')
:css('text-align', data.infoTextAlign)
:css('font-size', data.infoFontSize)
:css('padding', data.infoPadding)
:css('height', data.infoHeight)
:css('line-height', data.infoLineHeight)
:css('color', data.infoColor)
:css('vertical-align', 'middle')
:cssText(data.infoOtherParams)
:addClass(data.infoClass)
:wikitext(data.info)
-- Render the second id html.
if data.showId2 then
tablerow:tag('th')
:css('border', '0')
:css('width', data.id2Width)
:css('height', data.id2Height)
:css('background', data.id2BackgroundColor)
:css('text-align', data.id2TextAlign)
:css('font-size', data.id2FontSize)
:css('color', data.id2Color)
:css('padding', data.id2Padding)
:css('line-height', data.id2LineHeight)
:css('vertical-align', 'middle')
:cssText(data.id2OtherParams)
:wikitext(data.id2)
end
local title = mw.title.getCurrentTitle()
if (title.namespace == 2) and not title.text:match("/") then
return tostring(root) -- regular user page
elseif title.namespace == 14 then
return tostring(root) -- category
elseif title.isTalkPage then
return tostring(root) -- talk page
end
local function has_text(wikitext)
local function get_alt(text)
return text:match("|alt=([^|]*)") or ""
end
wikitext = wikitext:gsub("]]", "|]]")
wikitext = wikitext:gsub("%[%[%s*[Mm][Ee][Dd][Ii][Aa]%s*:[^|]-(|.-)]]", get_alt)
wikitext = wikitext:gsub("%[%[%s*[Ii][Mm][Aa][Gg][Ee]%s*:[^|]-(|.-)]]", get_alt)
wikitext = wikitext:gsub("%[%[%s*[Ff][Ii][Ll][Ee]%s*:[^|]-(|.-)]]", get_alt)
return mw.text.trim(wikitext) ~= ""
end
return tostring(root)
end
function p.categories(args, page)
-- Gets categories
-- The page parameter makes the function act as though the module was being called from that page.
-- It is included for testing purposes.
local cats = {}
cats[#cats + 1] = args.usercategory
cats[#cats + 1] = args.usercategory2
cats[#cats + 1] = args.usercategory3
if #cats > 0 and not require("Module:Yesno")(args.nocat) then
-- Get the title object
local title
if page then
title = mw.title.new(page)
else
title = mw.title.getCurrentTitle()
end
-- Build category handler arguments.
local chargs = {}
chargs.page = page
chargs.main = '[[Category:Pages with misplaced templates]]'
if title.namespace == 2 then
-- User namespace.
local user = ''
for i, cat in ipairs(cats) do
user = user .. makeCat(cat)
end
return user
elseif title.namespace == 10 then
-- Template namespace.
local basepage = title.baseText
local template = ''
for i, cat in ipairs(cats) do
template = template .. makeCat(cat, ' ' .. basepage)
end
return template
end
end
end
return p
aac333efff739f0243d8ffced6f4296cffb8d7e9
Template:Infobox
10
17
27
2022-09-30T14:45:57Z
dev>Pppery
0
Copy from Wikipedia
wikitext
text/x-wiki
{{#invoke:Infobox|infobox}}<noinclude>
{{documentation}}
</noinclude>
627ee6fcf4d4f108fe054b5c476201cad0ed7717
Module:Infobox
828
19
31
2022-09-30T14:52:23Z
dev>Pppery
0
Scribunto
text/plain
--
-- This module implements {{Infobox}}
--
local p = {}
local args = {}
local origArgs = {}
local root
local function notempty( s ) return s and s:match( '%S' ) end
local function fixChildBoxes(sval, tt)
if notempty(sval) then
local marker = '<span class=special_infobox_marker>'
local s = sval
s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1')
s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker)
if s:match(marker) then
s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '')
s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1')
s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1')
s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1')
s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1')
end
if s:match(marker) then
local subcells = mw.text.split(s, marker)
s = ''
for k = 1, #subcells do
if k == 1 then
s = s .. subcells[k] .. '</' .. tt .. '></tr>'
elseif k == #subcells then
local rowstyle = ' style="display:none"'
if notempty(subcells[k]) then rowstyle = '' end
s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' .. subcells[k]
elseif notempty(subcells[k]) then
if (k % 2) == 0 then
s = s .. subcells[k]
else
s = s .. '<tr><' .. tt .. ' colspan=2>\n' .. subcells[k] .. '</' .. tt .. '></tr>'
end
end
end
end
-- the next two lines add a newline at the end of lists for the PHP parser
-- https://en.wikipedia.org/w/index.php?title=Template_talk:Infobox_musical_artist&oldid=849054481
-- remove when [[:phab:T191516]] is fixed or OBE
s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n')
s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n')
s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1')
s = mw.ustring.gsub(s, '^(%{%|)', '\n%1')
return s
else
return sval
end
end
local function union(t1, t2)
-- Returns the union of the values of two tables, as a sequence.
local vals = {}
for k, v in pairs(t1) do
vals[v] = true
end
for k, v in pairs(t2) do
vals[v] = true
end
local ret = {}
for k, v in pairs(vals) do
table.insert(ret, k)
end
return ret
end
local function getArgNums(prefix)
-- Returns a table containing the numbers of the arguments that exist
-- for the specified prefix. For example, if the prefix was 'data', and
-- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
local nums = {}
for k, v in pairs(args) do
local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$')
if num then table.insert(nums, tonumber(num)) end
end
table.sort(nums)
return nums
end
local function addRow(rowArgs)
-- Adds a row to the infobox, with either a header cell
-- or a label/data cell combination.
if rowArgs.header and rowArgs.header ~= '_BLANK_' then
root
:tag('tr')
:addClass(rowArgs.rowclass)
:cssText(rowArgs.rowstyle)
:attr('id', rowArgs.rowid)
:tag('th')
:attr('colspan', 2)
:attr('id', rowArgs.headerid)
:addClass(rowArgs.class)
:addClass(args.headerclass)
:css('text-align', 'center')
:cssText(args.headerstyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(fixChildBoxes(rowArgs.header, 'th'))
elseif rowArgs.data then
if not rowArgs.data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then
rowArgs.rowstyle = 'display:none'
end
local row = root:tag('tr')
row:addClass(rowArgs.rowclass)
row:cssText(rowArgs.rowstyle)
row:attr('id', rowArgs.rowid)
if rowArgs.label then
row
:tag('th')
:attr('scope', 'row')
:attr('id', rowArgs.labelid)
:cssText(args.labelstyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(rowArgs.label)
:done()
end
local dataCell = row:tag('td')
if not rowArgs.label then
dataCell
:attr('colspan', 2)
:css('text-align', 'center')
end
dataCell
:attr('id', rowArgs.dataid)
:addClass(rowArgs.class)
:cssText(rowArgs.datastyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(fixChildBoxes(rowArgs.data, 'td'))
end
end
local function renderTitle()
if not args.title then return end
root
:tag('caption')
:addClass(args.titleclass)
:cssText(args.titlestyle)
:wikitext(args.title)
end
local function renderAboveRow()
if not args.above then return end
root
:tag('tr')
:tag('th')
:attr('colspan', 2)
:addClass(args.aboveclass)
:css('text-align', 'center')
:css('font-size', '125%')
:css('font-weight', 'bold')
:cssText(args.abovestyle)
:wikitext(fixChildBoxes(args.above,'th'))
end
local function renderBelowRow()
if not args.below then return end
root
:tag('tr')
:tag('td')
:attr('colspan', '2')
:addClass(args.belowclass)
:css('text-align', 'center')
:cssText(args.belowstyle)
:wikitext(fixChildBoxes(args.below,'td'))
end
local function renderSubheaders()
if args.subheader then
args.subheader1 = args.subheader
end
if args.subheaderrowclass then
args.subheaderrowclass1 = args.subheaderrowclass
end
local subheadernums = getArgNums('subheader')
for k, num in ipairs(subheadernums) do
addRow({
data = args['subheader' .. tostring(num)],
datastyle = args.subheaderstyle,
rowcellstyle = args['subheaderstyle' .. tostring(num)],
class = args.subheaderclass,
rowclass = args['subheaderrowclass' .. tostring(num)]
})
end
end
local function renderImages()
if args.image then
args.image1 = args.image
end
if args.caption then
args.caption1 = args.caption
end
local imagenums = getArgNums('image')
for k, num in ipairs(imagenums) do
local caption = args['caption' .. tostring(num)]
local data = mw.html.create():wikitext(args['image' .. tostring(num)])
if caption then
data
:tag('div')
:cssText(args.captionstyle)
:wikitext(caption)
end
addRow({
data = tostring(data),
datastyle = args.imagestyle,
class = args.imageclass,
rowclass = args['imagerowclass' .. tostring(num)]
})
end
end
local function preprocessRows()
-- Gets the union of the header and data argument numbers,
-- and renders them all in order using addRow.
local rownums = union(getArgNums('header'), getArgNums('data'))
table.sort(rownums)
local lastheader
for k, num in ipairs(rownums) do
if args['header' .. tostring(num)] then
if lastheader then
args['header' .. tostring(lastheader)] = nil
end
lastheader = num
elseif args['data' .. tostring(num)] and args['data' .. tostring(num)]:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then
local data = args['data' .. tostring(num)]
if data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('%S') then
lastheader = nil
end
end
end
if lastheader then
args['header' .. tostring(lastheader)] = nil
end
end
local function renderRows()
-- Gets the union of the header and data argument numbers,
-- and renders them all in order using addRow.
local rownums = union(getArgNums('header'), getArgNums('data'))
table.sort(rownums)
for k, num in ipairs(rownums) do
addRow({
header = args['header' .. tostring(num)],
label = args['label' .. tostring(num)],
data = args['data' .. tostring(num)],
datastyle = args.datastyle,
class = args['class' .. tostring(num)],
rowclass = args['rowclass' .. tostring(num)],
rowstyle = args['rowstyle' .. tostring(num)],
rowcellstyle = args['rowcellstyle' .. tostring(num)],
dataid = args['dataid' .. tostring(num)],
labelid = args['labelid' .. tostring(num)],
headerid = args['headerid' .. tostring(num)],
rowid = args['rowid' .. tostring(num)]
})
end
end
local function renderItalicTitle()
local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title'])
if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then
root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'}))
end
end
local function _infobox()
-- Specify the overall layout of the infobox, with special settings
-- if the infobox is used as a 'child' inside another infobox.
if args.child ~= 'yes' then
root = mw.html.create('table')
root
:addClass((args.subbox ~= 'yes') and 'infobox' or nil)
:addClass(args.bodyclass)
if args.subbox == 'yes' then
root
:css('padding', '0')
:css('border', 'none')
:css('margin', '-3px')
:css('width', 'auto')
:css('min-width', '100%')
:css('font-size', '100%')
:css('clear', 'none')
:css('float', 'none')
:css('background-color', 'transparent')
else
root
:css('width', '22em')
end
root
:cssText(args.bodystyle)
renderTitle()
renderAboveRow()
else
root = mw.html.create()
root
:wikitext(args.title)
end
renderSubheaders()
renderImages()
if args.autoheaders then
preprocessRows()
end
renderRows()
renderBelowRow()
renderItalicTitle()
return tostring(root)
end
local function preprocessSingleArg(argName)
-- If the argument exists and isn't blank, add it to the argument table.
-- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
if origArgs[argName] and origArgs[argName] ~= '' then
args[argName] = origArgs[argName]
end
end
local function preprocessArgs(prefixTable, step)
-- Assign the parameters with the given prefixes to the args table, in order, in batches
-- of the step size specified. This is to prevent references etc. from appearing in the
-- wrong order. The prefixTable should be an array containing tables, each of which has
-- two possible fields, a "prefix" string and a "depend" table. The function always parses
-- parameters containing the "prefix" string, but only parses parameters in the "depend"
-- table if the prefix parameter is present and non-blank.
if type(prefixTable) ~= 'table' then
error("Non-table value detected for the prefix table", 2)
end
if type(step) ~= 'number' then
error("Invalid step value detected", 2)
end
-- Get arguments without a number suffix, and check for bad input.
for i,v in ipairs(prefixTable) do
if type(v) ~= 'table' or type(v.prefix) ~= "string" or (v.depend and type(v.depend) ~= 'table') then
error('Invalid input detected to preprocessArgs prefix table', 2)
end
preprocessSingleArg(v.prefix)
-- Only parse the depend parameter if the prefix parameter is present and not blank.
if args[v.prefix] and v.depend then
for j, dependValue in ipairs(v.depend) do
if type(dependValue) ~= 'string' then
error('Invalid "depend" parameter value detected in preprocessArgs')
end
preprocessSingleArg(dependValue)
end
end
end
-- Get arguments with number suffixes.
local a = 1 -- Counter variable.
local moreArgumentsExist = true
while moreArgumentsExist == true do
moreArgumentsExist = false
for i = a, a + step - 1 do
for j,v in ipairs(prefixTable) do
local prefixArgName = v.prefix .. tostring(i)
if origArgs[prefixArgName] then
moreArgumentsExist = true -- Do another loop if any arguments are found, even blank ones.
preprocessSingleArg(prefixArgName)
end
-- Process the depend table if the prefix argument is present and not blank, or
-- we are processing "prefix1" and "prefix" is present and not blank, and
-- if the depend table is present.
if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
for j,dependValue in ipairs(v.depend) do
local dependArgName = dependValue .. tostring(i)
preprocessSingleArg(dependArgName)
end
end
end
end
a = a + step
end
end
local function parseDataParameters()
-- Parse the data parameters in the same order that the old {{infobox}} did, so that
-- references etc. will display in the expected places. Parameters that depend on
-- another parameter are only processed if that parameter is present, to avoid
-- phantom references appearing in article reference lists.
preprocessSingleArg('autoheaders')
preprocessSingleArg('child')
preprocessSingleArg('bodyclass')
preprocessSingleArg('subbox')
preprocessSingleArg('bodystyle')
preprocessSingleArg('title')
preprocessSingleArg('titleclass')
preprocessSingleArg('titlestyle')
preprocessSingleArg('above')
preprocessSingleArg('aboveclass')
preprocessSingleArg('abovestyle')
preprocessArgs({
{prefix = 'subheader', depend = {'subheaderstyle', 'subheaderrowclass'}}
}, 10)
preprocessSingleArg('subheaderstyle')
preprocessSingleArg('subheaderclass')
preprocessArgs({
{prefix = 'image', depend = {'caption', 'imagerowclass'}}
}, 10)
preprocessSingleArg('captionstyle')
preprocessSingleArg('imagestyle')
preprocessSingleArg('imageclass')
preprocessArgs({
{prefix = 'header'},
{prefix = 'data', depend = {'label'}},
{prefix = 'rowclass'},
{prefix = 'rowstyle'},
{prefix = 'rowcellstyle'},
{prefix = 'class'},
{prefix = 'dataid'},
{prefix = 'labelid'},
{prefix = 'headerid'},
{prefix = 'rowid'}
}, 50)
preprocessSingleArg('headerclass')
preprocessSingleArg('headerstyle')
preprocessSingleArg('labelstyle')
preprocessSingleArg('datastyle')
preprocessSingleArg('below')
preprocessSingleArg('belowclass')
preprocessSingleArg('belowstyle')
preprocessSingleArg('name')
args['italic title'] = origArgs['italic title'] -- different behaviour if blank or absent
preprocessSingleArg('decat')
end
function p.infobox(frame)
-- If called via #invoke, use the args passed into the invoking template.
-- Otherwise, for testing purposes, assume args are being passed directly in.
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
parseDataParameters()
return _infobox()
end
function p.infoboxTemplate(frame)
-- For calling via #invoke within a template
origArgs = {}
for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end
parseDataParameters()
return _infobox()
end
return p
c6ac51f9e2faf9c2f3aba1fb8c05af98db47f4d4
Template:Utc
10
79
150
2022-09-30T16:16:46Z
dev>Pppery
0
36 revisions imported from [[:wikipedia:Template:Utc]]
wikitext
text/x-wiki
{{#time:H:i|{{#expr:{{{1|0}}} * 60 + {{{2|0}}} round 0}} minutes}}<noinclude>
{{documentation}}
<!-- PLEASE ADD CATEGORIES AND INTERWIKIS TO THE /doc SUBPAGE, THANKS -->
</noinclude>
d24309676bfe4692d038657a7171952e7d1cded7
Template:Template link expanded
10
75
142
2022-09-30T18:48:13Z
dev>Pppery
0
wikitext
text/x-wiki
<code><nowiki>{{</nowiki>{{#if:{{{subst|}}} |[[Help:Substitution|subst]]:}}<!--
-->[[{{{sister|{{{SISTER|}}}}}}{{ns:Template}}:{{{1|}}}|{{{1|}}}]]<!--
-->{{#if:{{{2|}}} ||{{{2}}}}}<!--
-->{{#if:{{{3|}}} ||{{{3}}}}}<!--
-->{{#if:{{{4|}}} ||{{{4}}}}}<!--
-->{{#if:{{{5|}}} ||{{{5}}}}}<!--
-->{{#if:{{{6|}}} ||{{{6}}}}}<!--
-->{{#if:{{{7|}}} ||{{{7}}}}}<!--
-->{{#if:{{{8|}}} ||{{{8}}}}}<!--
-->{{#if:{{{9|}}} ||{{{9}}}}}<!--
-->{{#if:{{{10|}}} ||{{{10}}}}}<!--
-->{{#if:{{{11|}}} ||{{{11}}}}}<!--
-->{{#if:{{{12|}}} ||{{{12}}}}}<!--
-->{{#if:{{{13|}}} ||{{{13}}}}}<!--
-->{{#if:{{{14|}}} ||{{{14}}}}}<!--
-->{{#if:{{{15|}}} ||{{{15}}}}}<!--
-->{{#if:{{{16|}}} ||{{{16}}}}}<!--
-->{{#if:{{{17|}}} ||{{{17}}}}}<!--
-->{{#if:{{{18|}}} ||{{{18}}}}}<!--
-->{{#if:{{{19|}}} ||{{{19}}}}}<!--
-->{{#if:{{{20|}}} ||{{{20}}}}}<!--
-->{{#if:{{{21|}}} ||''...''}}<!--
--><nowiki>}}</nowiki></code><noinclude>
{{Documentation}}
</noinclude>
9f670205d4b358df089b1a820f78f02a88afca3a
Template:User instagram
10
69
130
2022-10-01T17:13:01Z
dev>Pppery
0
wikitext
text/x-wiki
{{Userbox
| id = [[File:Instagram icon.png|37px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an {{#if:{{{account|}}}| account [https://instagram.com/{{{account}}} '''@{{{account}}}''']|account}} on Instagram.
| nocat = {{{nocat|}}}
| usercategory = Users who use Instagram
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
1178ad0721de804c08dd554ebb4b52c4c6569fde
Template:User IRC
10
65
122
2022-10-01T17:13:23Z
dev>Pppery
0
wikitext
text/x-wiki
{{Userbox
| id = #
| id-s = 24
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:IRC|IRC]]{{#if:{{{nick|}}}| as ''{{{nick}}}''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use IRC
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
a148152ff16bb6fc7f7a7bd46c4898b50f1996fc
Module:Yesno
828
95
182
2022-10-01T17:25:37Z
dev>Pppery
0
Pppery moved page [[Module:Yesno/2]] to [[Module:Yesno]] without leaving a redirect
Scribunto
text/plain
-- Function allowing for consistent treatment of boolean-like wikitext input.
-- It works similarly to the template {{yesno}}.
return function (val, default)
-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
-- should replace "val:lower()" with "mw.ustring.lower(val)" in the
-- following line.
val = type(val) == 'string' and val:lower() or val
if val == nil then
return nil
elseif val == true
or val == 'yes'
or val == 'y'
or val == 'true'
or val == 't'
or val == 'on'
or tonumber(val) == 1
then
return true
elseif val == false
or val == 'no'
or val == 'n'
or val == 'false'
or val == 'f'
or val == 'off'
or tonumber(val) == 0
then
return false
else
return default
end
end
f767643e7d12126d020d88d662a3dd057817b9dc
Module:No globals
828
87
166
2022-10-01T17:28:48Z
dev>Pppery
0
Pppery moved page [[Module:No globals/2]] to [[Module:No globals]] without leaving a redirect
Scribunto
text/plain
local mt = getmetatable(_G) or {}
function mt.__index (t, k)
if k ~= 'arg' then
error('Tried to read nil global ' .. tostring(k), 2)
end
return nil
end
function mt.__newindex(t, k, v)
if k ~= 'arg' then
error('Tried to write global ' .. tostring(k), 2)
end
rawset(t, k, v)
end
setmetatable(_G, mt)
8ce3969f7d53b08bd00dabe4cc9780bc6afd412a
Module:Documentation/config
828
22
37
2022-10-01T17:37:53Z
dev>Pppery
0
Pppery moved page [[Module:Documentation/config/2]] to [[Module:Documentation/config]] without leaving a redirect
Scribunto
text/plain
----------------------------------------------------------------------------------------------------
--
-- Configuration for Module:Documentation
--
-- Here you can set the values of the parameters and messages used in Module:Documentation to
-- localise it to your wiki and your language. Unless specified otherwise, values given here
-- should be string values.
----------------------------------------------------------------------------------------------------
local cfg = {} -- Do not edit this line.
----------------------------------------------------------------------------------------------------
-- Start box configuration
----------------------------------------------------------------------------------------------------
-- cfg['documentation-icon-wikitext']
-- The wikitext for the icon shown at the top of the template.
cfg['documentation-icon-wikitext'] = '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]'
-- cfg['template-namespace-heading']
-- The heading shown in the template namespace.
cfg['template-namespace-heading'] = 'Template documentation'
-- cfg['module-namespace-heading']
-- The heading shown in the module namespace.
cfg['module-namespace-heading'] = 'Module documentation'
-- cfg['file-namespace-heading']
-- The heading shown in the file namespace.
cfg['file-namespace-heading'] = 'Summary'
-- cfg['other-namespaces-heading']
-- The heading shown in other namespaces.
cfg['other-namespaces-heading'] = 'Documentation'
-- cfg['view-link-display']
-- The text to display for "view" links.
cfg['view-link-display'] = 'view'
-- cfg['edit-link-display']
-- The text to display for "edit" links.
cfg['edit-link-display'] = 'edit'
-- cfg['history-link-display']
-- The text to display for "history" links.
cfg['history-link-display'] = 'history'
-- cfg['purge-link-display']
-- The text to display for "purge" links.
cfg['purge-link-display'] = 'purge'
-- cfg['create-link-display']
-- The text to display for "create" links.
cfg['create-link-display'] = 'create'
----------------------------------------------------------------------------------------------------
-- Link box (end box) configuration
----------------------------------------------------------------------------------------------------
-- cfg['transcluded-from-blurb']
-- Notice displayed when the docs are transcluded from another page. $1 is a wikilink to that page.
cfg['transcluded-from-blurb'] = 'The above [[w:Wikipedia:Template documentation|documentation]] is [[mw:Help:Transclusion|transcluded]] from $1.'
--[[
-- cfg['create-module-doc-blurb']
-- Notice displayed in the module namespace when the documentation subpage does not exist.
-- $1 is a link to create the documentation page with the preload cfg['module-preload'] and the
-- display cfg['create-link-display'].
--]]
cfg['create-module-doc-blurb'] = 'You might want to $1 a documentation page for this [[mw:Extension:Scribunto/Lua reference manual|Scribunto module]].'
----------------------------------------------------------------------------------------------------
-- Experiment blurb configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['experiment-blurb-template']
-- cfg['experiment-blurb-module']
-- The experiment blurb is the text inviting editors to experiment in sandbox and test cases pages.
-- It is only shown in the template and module namespaces. With the default English settings, it
-- might look like this:
--
-- Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages.
--
-- In this example, "sandbox", "edit", "diff", "testcases", and "edit" would all be links.
--
-- There are two versions, cfg['experiment-blurb-template'] and cfg['experiment-blurb-module'], depending
-- on what namespace we are in.
--
-- Parameters:
--
-- $1 is a link to the sandbox page. If the sandbox exists, it is in the following format:
--
-- cfg['sandbox-link-display'] (cfg['sandbox-edit-link-display'] | cfg['compare-link-display'])
--
-- If the sandbox doesn't exist, it is in the format:
--
-- cfg['sandbox-link-display'] (cfg['sandbox-create-link-display'] | cfg['mirror-link-display'])
--
-- The link for cfg['sandbox-create-link-display'] link preloads the page with cfg['template-sandbox-preload']
-- or cfg['module-sandbox-preload'], depending on the current namespace. The link for cfg['mirror-link-display']
-- loads a default edit summary of cfg['mirror-edit-summary'].
--
-- $2 is a link to the test cases page. If the test cases page exists, it is in the following format:
--
-- cfg['testcases-link-display'] (cfg['testcases-edit-link-display'] | cfg['testcases-run-link-display'])
--
-- If the test cases page doesn't exist, it is in the format:
--
-- cfg['testcases-link-display'] (cfg['testcases-create-link-display'])
--
-- If the test cases page doesn't exist, the link for cfg['testcases-create-link-display'] preloads the
-- page with cfg['template-testcases-preload'] or cfg['module-testcases-preload'], depending on the current
-- namespace.
--]]
cfg['experiment-blurb-template'] = "Editors can experiment in this template's $1 and $2 pages."
cfg['experiment-blurb-module'] = "Editors can experiment in this module's $1 and $2 pages."
----------------------------------------------------------------------------------------------------
-- Sandbox link configuration
----------------------------------------------------------------------------------------------------
-- cfg['sandbox-subpage']
-- The name of the template subpage typically used for sandboxes.
cfg['sandbox-subpage'] = 'sandbox'
-- cfg['template-sandbox-preload']
-- Preload file for template sandbox pages.
cfg['template-sandbox-preload'] = 'Template:Documentation/preload-sandbox'
-- cfg['module-sandbox-preload']
-- Preload file for Lua module sandbox pages.
cfg['module-sandbox-preload'] = 'Template:Documentation/preload-module-sandbox'
-- cfg['sandbox-link-display']
-- The text to display for "sandbox" links.
cfg['sandbox-link-display'] = 'sandbox'
-- cfg['sandbox-edit-link-display']
-- The text to display for sandbox "edit" links.
cfg['sandbox-edit-link-display'] = 'edit'
-- cfg['sandbox-create-link-display']
-- The text to display for sandbox "create" links.
cfg['sandbox-create-link-display'] = 'create'
-- cfg['compare-link-display']
-- The text to display for "compare" links.
cfg['compare-link-display'] = 'diff'
-- cfg['mirror-edit-summary']
-- The default edit summary to use when a user clicks the "mirror" link. $1 is a wikilink to the
-- template page.
cfg['mirror-edit-summary'] = 'Create sandbox version of $1'
-- cfg['mirror-link-display']
-- The text to display for "mirror" links.
cfg['mirror-link-display'] = 'mirror'
-- cfg['mirror-link-preload']
-- The page to preload when a user clicks the "mirror" link.
cfg['mirror-link-preload'] = 'Template:Documentation/mirror'
----------------------------------------------------------------------------------------------------
-- Test cases link configuration
----------------------------------------------------------------------------------------------------
-- cfg['testcases-subpage']
-- The name of the template subpage typically used for test cases.
cfg['testcases-subpage'] = 'testcases'
-- cfg['template-testcases-preload']
-- Preload file for template test cases pages.
cfg['template-testcases-preload'] = 'Template:Documentation/preload-testcases'
-- cfg['module-testcases-preload']
-- Preload file for Lua module test cases pages.
cfg['module-testcases-preload'] = 'Template:Documentation/preload-module-testcases'
-- cfg['testcases-link-display']
-- The text to display for "testcases" links.
cfg['testcases-link-display'] = 'testcases'
-- cfg['testcases-edit-link-display']
-- The text to display for test cases "edit" links.
cfg['testcases-edit-link-display'] = 'edit'
-- cfg['testcases-run-link-display']
-- The text to display for test cases "run" links.
cfg['testcases-run-link-display'] = 'run'
-- cfg['testcases-create-link-display']
-- The text to display for test cases "create" links.
cfg['testcases-create-link-display'] = 'create'
----------------------------------------------------------------------------------------------------
-- Add categories blurb configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['add-categories-blurb']
-- Text to direct users to add categories to the /doc subpage. Not used if the "content" or
-- "docname fed" arguments are set, as then it is not clear where to add the categories. $1 is a
-- link to the /doc subpage with a display value of cfg['doc-link-display'].
--]]
cfg['add-categories-blurb'] = 'Add categories to the $1 subpage.'
-- cfg['doc-link-display']
-- The text to display when linking to the /doc subpage.
cfg['doc-link-display'] = '/doc'
----------------------------------------------------------------------------------------------------
-- Subpages link configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['subpages-blurb']
-- The "Subpages of this template" blurb. $1 is a link to the main template's subpages with a
-- display value of cfg['subpages-link-display']. In the English version this blurb is simply
-- the link followed by a period, and the link display provides the actual text.
--]]
cfg['subpages-blurb'] = '$1.'
--[[
-- cfg['subpages-link-display']
-- The text to display for the "subpages of this page" link. $1 is cfg['template-pagetype'],
-- cfg['module-pagetype'] or cfg['default-pagetype'], depending on whether the current page is in
-- the template namespace, the module namespace, or another namespace.
--]]
cfg['subpages-link-display'] = 'Subpages of this $1'
-- cfg['template-pagetype']
-- The pagetype to display for template pages.
cfg['template-pagetype'] = 'template'
-- cfg['module-pagetype']
-- The pagetype to display for Lua module pages.
cfg['module-pagetype'] = 'module'
-- cfg['default-pagetype']
-- The pagetype to display for pages other than templates or Lua modules.
cfg['default-pagetype'] = 'page'
----------------------------------------------------------------------------------------------------
-- Doc link configuration
----------------------------------------------------------------------------------------------------
-- cfg['doc-subpage']
-- The name of the subpage typically used for documentation pages.
cfg['doc-subpage'] = 'doc'
-- cfg['docpage-preload']
-- Preload file for template documentation pages in all namespaces.
cfg['docpage-preload'] = 'Template:Documentation/preload'
-- cfg['module-preload']
-- Preload file for Lua module documentation pages.
cfg['module-preload'] = 'Template:Documentation/preload-module-doc'
----------------------------------------------------------------------------------------------------
-- HTML and CSS configuration
----------------------------------------------------------------------------------------------------
-- cfg['templatestyles']
-- The name of the TemplateStyles page where CSS is kept.
-- Sandbox CSS will be at Module:Documentation/sandbox/styles.css when needed.
cfg['templatestyles'] = 'Module:Documentation/styles.css'
-- cfg['container']
-- Class which can be used to set flex or grid CSS on the
-- two child divs documentation and documentation-metadata
cfg['container'] = 'documentation-container'
-- cfg['main-div-classes']
-- Classes added to the main HTML "div" tag.
cfg['main-div-classes'] = 'documentation'
-- cfg['main-div-heading-class']
-- Class for the main heading for templates and modules and assoc. talk spaces
cfg['main-div-heading-class'] = 'documentation-heading'
-- cfg['start-box-class']
-- Class for the start box
cfg['start-box-class'] = 'documentation-startbox'
-- cfg['start-box-link-classes']
-- Classes used for the [view][edit][history] or [create] links in the start box.
-- mw-editsection-like is per [[Wikipedia:Village pump (technical)/Archive 117]]
cfg['start-box-link-classes'] = 'mw-editsection-like plainlinks'
-- cfg['end-box-class']
-- Class for the end box.
cfg['end-box-class'] = 'documentation-metadata'
-- cfg['end-box-plainlinks']
-- Plainlinks
cfg['end-box-plainlinks'] = 'plainlinks'
-- cfg['toolbar-class']
-- Class added for toolbar links.
cfg['toolbar-class'] = 'documentation-toolbar'
-- cfg['clear']
-- Just used to clear things.
cfg['clear'] = 'documentation-clear'
----------------------------------------------------------------------------------------------------
-- Tracking category configuration
----------------------------------------------------------------------------------------------------
-- cfg['display-strange-usage-category']
-- Set to true to enable output of cfg['strange-usage-category'] if the module is used on a /doc subpage
-- or a /testcases subpage. This should be a boolean value (either true or false).
cfg['display-strange-usage-category'] = true
-- cfg['strange-usage-category']
-- Category to output if cfg['display-strange-usage-category'] is set to true and the module is used on a
-- /doc subpage or a /testcases subpage.
cfg['strange-usage-category'] = 'Wikipedia pages with strange ((documentation)) usage'
--[[
----------------------------------------------------------------------------------------------------
-- End configuration
--
-- Don't edit anything below this line.
----------------------------------------------------------------------------------------------------
--]]
return cfg
d70e8b1402a2bbe08a1fef4b75d743e661af0c95
Template:Documentation subpage
10
70
132
2022-10-01T17:51:17Z
dev>Pppery
0
wikitext
text/x-wiki
<includeonly><!--
-->{{#ifeq:{{lc:{{SUBPAGENAME}}}} |{{{override|doc}}}
| <!--(this template has been transcluded on a /doc or /{{{override}}} page)-->
</includeonly><!--
-->{{#ifeq:{{{doc-notice|show}}} |show
| {{Mbox
| type = notice
| style = margin-bottom:1.0em;
| image = [[File:Edit-copy green.svg|40px|alt=|link=]]
| text =
'''This is a documentation subpage''' for '''{{{1|[[:{{SUBJECTSPACE}}:{{BASEPAGENAME}}]]}}}'''.<br/> It contains usage information, [[mw:Help:Categories|categories]] and other content that is not part of the original {{#if:{{{text2|}}} |{{{text2}}} |{{#if:{{{text1|}}} |{{{text1}}} | page}}}}.
}}
}}<!--
-->{{DEFAULTSORT:{{{defaultsort|{{PAGENAME}}}}}}}<!--
-->{{#if:{{{inhibit|}}} |<!--(don't categorize)-->
| <includeonly><!--
-->{{#ifexist:{{NAMESPACE}}:{{BASEPAGENAME}}
| [[Category:{{#switch:{{SUBJECTSPACE}} |Template=Template |Module=Module |User=User |#default=Wikipedia}} documentation pages]]
| [[Category:Documentation subpages without corresponding pages]]
}}<!--
--></includeonly>
}}<!--
(completing initial #ifeq: at start of template:)
--><includeonly>
| <!--(this template has not been transcluded on a /doc or /{{{override}}} page)-->
}}<!--
--></includeonly><noinclude>{{Documentation}}</noinclude>
471e685c1c643a5c6272e20e49824fffebad0448
Template:User youtube
10
68
128
2022-10-02T01:37:31Z
dev>Tali64³
0
Made social media template for YouTube
wikitext
text/x-wiki
{{Userbox
| id = [[File:YouTube full-color icon (2017).svg|37px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has a YouTube channel{{#if:{{{account|}}}| at [https://{{{account}}} '''{{{account}}}''']|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use YouTube
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
f0ba1080f2a2d69494317a9790fa3d7e6e4239b4
Template:User github
10
64
120
2022-10-04T16:14:11Z
dev>MirahezeGDPR a51581232c7cc84ec1a32c40d8489548
0
More consistent with other userboxes + this is only supposed to be for accounts
wikitext
text/x-wiki
{{Userbox
| id = [[File:GitLogo.png|43px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an account on GitHub{{#if:{{{account|}}}| as ''[[github:{{{account}}}|{{{account}}}]]''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use GitHub
}}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
08ef531d5b5a1e69b84939e0fc1f1d0d622f38ad
Template:Hatnote
10
80
152
2022-10-05T21:18:12Z
dev>Pppery
0
Category
wikitext
text/x-wiki
<div style="margin-left:2em; margin-right: 2em;>''{{{1}}}''</div>
<!-- The wikipedia templates uses :, which generated dd and dt tags. That is not ideal for accessibility. --><noinclude>
This is a general purpose template for all kind of [https://en.wikipedia.org/wiki/Wikipedia:Hatnote hatnotes]. '''Hatnotes''' are a small annotation above a page or a section that can help the reader navigate. It's used for example to clarify what a section is about and link to other pages the reader may want to read.
== Example usage ==
<pre><nowiki>
=== Heading ===
{{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}}
The first sentence of the section is here.
</nowiki></pre>
=== Heading ===
{{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}}
The first sentence of the section is here.
<templatedata>
{
"params": {
"1": {
"label": "content",
"description": "the content of the note",
"example": "For the xxx see [[yyy]]."
}
},
"description": "Adds a annotation for the reader about the content that follows. Usually used after a heading."
}
</templatedata>
[[Category:Templates]]
</noinclude>
5e58f83d6fa53ed06f85139184aff1d651804efe
Template:MessageBox
10
72
136
2022-10-05T21:20:15Z
dev>Pppery
0
Categorize
wikitext
text/x-wiki
<div style="width: {{#if:{{{width|}}}|{{{width}}}|80%}}; background-color: {{#if:{{{Background color}}}|{{{Background color}}}|#f5f5f5}}; border-top: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-bottom: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-right: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-left: 12px solid {{#if:{{{Flag color}}}|{{{Flag color}}}|#aaaaaa}}; margin: 0.5em auto 0.5em;">
{|
{{#if:{{{Image}}}|{{!}}style="width:93px; text-align:center; vertical-align:middle; padding-top:1px;padding-bottom:7px" {{!}} {{{Image}}} }}
|style="vertical-align:middle;padding-left:3px;padding-top:10px;padding-bottom:10px;padding-right:10px; background-color: {{#if:{{{Background color}}}{{!}}{{{Background color}}}{{!}}#f5f5f5}};" | {{{Message text}}}
|}
</div><noinclude>[[Category:Notice templates]]</noinclude>
c6727bf6179a36a5413ed93f232fd0e2f7180256
Template:Shortcut
10
73
138
2022-10-05T21:21:21Z
dev>Pppery
0
Add doc
wikitext
text/x-wiki
<!--
Putting anchors on page:
--><div style="position: relative; top: -3em;">{{#if:{{{1|}}}|<span id="{{anchorencode:{{{1|}}}}}"></span> }}{{#if:{{{2|}}}|<span id="{{anchorencode:{{{2|}}}}}"></span> }}{{#if:{{{3|}}}|<span id="{{anchorencode:{{{3|}}}}}"></span> }}{{#if:{{{4|}}}|<span id="{{anchorencode:{{{4|}}}}}"></span> }}{{#if:{{{5|}}}|<span id="{{anchorencode:{{{5|}}}}}"></span> }}</div>
<table class="shortcutbox noprint" style="float: right; border: 1px solid #aaa; background: #fff; margin: .3em .3em .3em 1em; padding: 3px; text-align: center;"><tr><th style="border: none; background: transparent;" class="plainlist"><!--
Adding the shortcut links:
--><small>[[w:Wikipedia:Shortcut|Shortcut{{#if:{{{2|}}}|s}}]]:
{{#if:{{{1|}}}|<ul><li> [[{{{1}}}]]</li>
}}{{#if:{{{2|}}}|<li> [[{{{2}}}]]</li>
}}{{#if:{{{3|}}}|<li> [[{{{3}}}]]</li>
}}{{#if:{{{4|}}}|<li> [[{{{4}}}]]</li>
}}{{#if:{{{5|}}}|<li> [[{{{5}}}]]</li>
}}{{#if:{{{msg|}}}|<li> {{{msg}}}</li>
}}</ul></small></th></tr></table><noinclude>{{doc}}</noinclude>
96a4c79718f5cbdf3e074cc545193ea4e863d1fb
Template:Userbox
10
51
94
2022-10-05T21:25:59Z
dev>Pppery
0
Already on doc
wikitext
text/x-wiki
{{#invoke:userbox|userbox}}<noinclude>{{documentation}}</noinclude>
6813e8e31cadc62df2379b5fae9ea23c23f29e97
Module:Message box
828
90
172
2022-10-21T19:39:49Z
dev>Pppery
0
These can just go, the first for being very Wikipedia-specific, and the second for being almost impossible to import properly
Scribunto
text/plain
-- This is a meta-module for producing message box templates, including
-- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
-- Load necessary modules.
require('Module:No globals')
local getArgs
local yesno = require('Module:Yesno')
local templatestyles = 'Module:Message box/styles.css'
-- Get a language object for formatDate and ucfirst.
local lang = mw.language.getContentLanguage()
-- Define constants
local CONFIG_MODULE = 'Module:Message box/configuration'
local DEMOSPACES = {user = 'tmbox', talk = 'tmbox', image = 'imbox', file = 'imbox', category = 'cmbox', article = 'ambox', main = 'ambox'}
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function getTitleObject(...)
-- Get the title object, passing the function through pcall
-- in case we are over the expensive function count limit.
local success, title = pcall(mw.title.new, ...)
if success then
return title
end
end
local function union(t1, t2)
-- Returns the union of two arrays.
local vals = {}
for i, v in ipairs(t1) do
vals[v] = true
end
for i, v in ipairs(t2) do
vals[v] = true
end
local ret = {}
for k in pairs(vals) do
table.insert(ret, k)
end
table.sort(ret)
return ret
end
local function getArgNums(args, prefix)
local nums = {}
for k, v in pairs(args) do
local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
if num then
table.insert(nums, tonumber(num))
end
end
table.sort(nums)
return nums
end
--------------------------------------------------------------------------------
-- Box class definition
--------------------------------------------------------------------------------
local MessageBox = {}
MessageBox.__index = MessageBox
function MessageBox.new(boxType, args, cfg)
args = args or {}
local obj = {}
-- Set the title object and the namespace.
obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle()
-- Set the config for our box type.
obj.cfg = cfg[boxType]
if not obj.cfg then
local ns = obj.title.namespace
-- boxType is "mbox" or invalid input
if args.demospace and args.demospace ~= '' then
-- implement demospace parameter of mbox
local demospace = string.lower(args.demospace)
if DEMOSPACES[demospace] then
-- use template from DEMOSPACES
obj.cfg = cfg[DEMOSPACES[demospace]]
elseif string.find( demospace, 'talk' ) then
-- demo as a talk page
obj.cfg = cfg.tmbox
else
-- default to ombox
obj.cfg = cfg.ombox
end
elseif ns == 0 then
obj.cfg = cfg.ambox -- main namespace
elseif ns == 6 then
obj.cfg = cfg.imbox -- file namespace
elseif ns == 14 then
obj.cfg = cfg.cmbox -- category namespace
else
local nsTable = mw.site.namespaces[ns]
if nsTable and nsTable.isTalk then
obj.cfg = cfg.tmbox -- any talk namespace
else
obj.cfg = cfg.ombox -- other namespaces or invalid input
end
end
end
-- Set the arguments, and remove all blank arguments except for the ones
-- listed in cfg.allowBlankParams.
do
local newArgs = {}
for k, v in pairs(args) do
if v ~= '' then
newArgs[k] = v
end
end
for i, param in ipairs(obj.cfg.allowBlankParams or {}) do
newArgs[param] = args[param]
end
obj.args = newArgs
end
-- Define internal data structure.
obj.categories = {}
obj.classes = {}
-- For lazy loading of [[Module:Category handler]].
obj.hasCategories = false
return setmetatable(obj, MessageBox)
end
function MessageBox:addCat(ns, cat, sort)
if not cat then
return nil
end
if sort then
cat = string.format('[[Category:%s|%s]]', cat, sort)
else
cat = string.format('[[Category:%s]]', cat)
end
self.hasCategories = true
self.categories[ns] = self.categories[ns] or {}
table.insert(self.categories[ns], cat)
end
function MessageBox:addClass(class)
if not class then
return nil
end
table.insert(self.classes, class)
end
function MessageBox:setParameters()
local args = self.args
local cfg = self.cfg
-- Get type data.
self.type = args.type
local typeData = cfg.types[self.type]
self.invalidTypeError = cfg.showInvalidTypeError
and self.type
and not typeData
typeData = typeData or cfg.types[cfg.default]
self.typeClass = typeData.class
self.typeImage = typeData.image
-- Find whether we are using a small message box.
self.isSmall = cfg.allowSmall and (
cfg.smallParam and args.small == cfg.smallParam
or not cfg.smallParam and yesno(args.small)
)
-- Add attributes, classes and styles.
self.id = args.id
self.name = args.name
if self.name then
self:addClass('box-' .. string.gsub(self.name,' ','_'))
end
if yesno(args.plainlinks) ~= false then
self:addClass('plainlinks')
end
for _, class in ipairs(cfg.classes or {}) do
self:addClass(class)
end
if self.isSmall then
self:addClass(cfg.smallClass or 'mbox-small')
end
self:addClass(self.typeClass)
self:addClass(args.class)
self.style = args.style
self.attrs = args.attrs
-- Set text style.
self.textstyle = args.textstyle
-- Find if we are on the template page or not. This functionality is only
-- used if useCollapsibleTextFields is set, or if both cfg.templateCategory
-- and cfg.templateCategoryRequireName are set.
self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
if self.useCollapsibleTextFields
or cfg.templateCategory
and cfg.templateCategoryRequireName
then
if self.name then
local templateName = mw.ustring.match(
self.name,
'^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$'
) or self.name
templateName = 'Template:' .. templateName
self.templateTitle = getTitleObject(templateName)
end
self.isTemplatePage = self.templateTitle
and mw.title.equals(self.title, self.templateTitle)
end
-- Process data for collapsible text fields. At the moment these are only
-- used in {{ambox}}.
if self.useCollapsibleTextFields then
-- Get the self.issue value.
if self.isSmall and args.smalltext then
self.issue = args.smalltext
else
local sect
if args.sect == '' then
sect = 'This ' .. (cfg.sectionDefault or 'page')
elseif type(args.sect) == 'string' then
sect = 'This ' .. args.sect
end
local issue = args.issue
issue = type(issue) == 'string' and issue ~= '' and issue or nil
local text = args.text
text = type(text) == 'string' and text or nil
local issues = {}
table.insert(issues, sect)
table.insert(issues, issue)
table.insert(issues, text)
self.issue = table.concat(issues, ' ')
end
-- Get the self.talk value.
local talk = args.talk
-- Show talk links on the template page or template subpages if the talk
-- parameter is blank.
if talk == ''
and self.templateTitle
and (
mw.title.equals(self.templateTitle, self.title)
or self.title:isSubpageOf(self.templateTitle)
)
then
talk = '#'
elseif talk == '' then
talk = nil
end
if talk then
-- If the talk value is a talk page, make a link to that page. Else
-- assume that it's a section heading, and make a link to the talk
-- page of the current page with that section heading.
local talkTitle = getTitleObject(talk)
local talkArgIsTalkPage = true
if not talkTitle or not talkTitle.isTalkPage then
talkArgIsTalkPage = false
talkTitle = getTitleObject(
self.title.text,
mw.site.namespaces[self.title.namespace].talk.id
)
end
if talkTitle and talkTitle.exists then
local talkText = 'Relevant discussion may be found on'
if talkArgIsTalkPage then
talkText = string.format(
'%s [[%s|%s]].',
talkText,
talk,
talkTitle.prefixedText
)
else
talkText = string.format(
'%s the [[%s#%s|talk page]].',
talkText,
talkTitle.prefixedText,
talk
)
end
self.talk = talkText
end
end
-- Get other values.
self.fix = args.fix ~= '' and args.fix or nil
local date
if args.date and args.date ~= '' then
date = args.date
elseif args.date == '' and self.isTemplatePage then
date = lang:formatDate('F Y')
end
if date then
self.date = string.format(" <small class='date-container'>''(<span class='date'>%s</span>)''</small>", date)
end
self.info = args.info
end
-- Set the non-collapsible text field. At the moment this is used by all box
-- types other than ambox, and also by ambox when small=yes.
if self.isSmall then
self.text = args.smalltext or args.text
else
self.text = args.text
end
-- Set the below row.
self.below = cfg.below and args.below
-- General image settings.
self.imageCellDiv = not self.isSmall and cfg.imageCellDiv
self.imageEmptyCell = cfg.imageEmptyCell
if cfg.imageEmptyCellStyle then
self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
end
-- Left image settings.
local imageLeft = self.isSmall and args.smallimage or args.image
if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
or not cfg.imageCheckBlank and imageLeft ~= 'none'
then
self.imageLeft = imageLeft
if not imageLeft then
local imageSize = self.isSmall
and (cfg.imageSmallSize or '30x30px')
or '40x40px'
self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage
or 'Imbox notice.png', imageSize)
end
end
-- Right image settings.
local imageRight = self.isSmall and args.smallimageright or args.imageright
if not (cfg.imageRightNone and imageRight == 'none') then
self.imageRight = imageRight
end
end
function MessageBox:setMainspaceCategories()
local args = self.args
local cfg = self.cfg
if not cfg.allowMainspaceCategories then
return nil
end
local nums = {}
for _, prefix in ipairs{'cat', 'category', 'all'} do
args[prefix .. '1'] = args[prefix]
nums = union(nums, getArgNums(args, prefix))
end
-- The following is roughly equivalent to the old {{Ambox/category}}.
local date = args.date
date = type(date) == 'string' and date
local preposition = 'from'
for _, num in ipairs(nums) do
local mainCat = args['cat' .. tostring(num)]
or args['category' .. tostring(num)]
local allCat = args['all' .. tostring(num)]
mainCat = type(mainCat) == 'string' and mainCat
allCat = type(allCat) == 'string' and allCat
if mainCat and date and date ~= '' then
local catTitle = string.format('%s %s %s', mainCat, preposition, date)
self:addCat(0, catTitle)
catTitle = getTitleObject('Category:' .. catTitle)
if not catTitle or not catTitle.exists then
self:addCat(0, 'Articles with invalid date parameter in template')
end
elseif mainCat and (not date or date == '') then
self:addCat(0, mainCat)
end
if allCat then
self:addCat(0, allCat)
end
end
end
function MessageBox:setTemplateCategories()
local args = self.args
local cfg = self.cfg
-- Add template categories.
if cfg.templateCategory then
if cfg.templateCategoryRequireName then
if self.isTemplatePage then
self:addCat(10, cfg.templateCategory)
end
elseif not self.title.isSubpage then
self:addCat(10, cfg.templateCategory)
end
end
-- Add template error categories.
if cfg.templateErrorCategory then
local templateErrorCategory = cfg.templateErrorCategory
local templateCat, templateSort
if not self.name and not self.title.isSubpage then
templateCat = templateErrorCategory
elseif self.isTemplatePage then
local paramsToCheck = cfg.templateErrorParamsToCheck or {}
local count = 0
for i, param in ipairs(paramsToCheck) do
if not args[param] then
count = count + 1
end
end
if count > 0 then
templateCat = templateErrorCategory
templateSort = tostring(count)
end
if self.categoryNums and #self.categoryNums > 0 then
templateCat = templateErrorCategory
templateSort = 'C'
end
end
self:addCat(10, templateCat, templateSort)
end
end
function MessageBox:setAllNamespaceCategories()
-- Set categories for all namespaces.
if self.invalidTypeError then
local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText
self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
end
end
function MessageBox:setCategories()
if self.title.namespace == 0 then
self:setMainspaceCategories()
elseif self.title.namespace == 10 then
self:setTemplateCategories()
end
self:setAllNamespaceCategories()
end
function MessageBox:renderCategories()
if not self.hasCategories then
-- No categories added, no need to pass them to Category handler so,
-- if it was invoked, it would return the empty string.
-- So we shortcut and return the empty string.
return ""
end
-- Convert category tables to strings and pass them through
-- [[Module:Category handler]].
return require('Module:Category handler')._main{
main = table.concat(self.categories[0] or {}),
template = table.concat(self.categories[10] or {}),
all = table.concat(self.categories.all or {}),
nocat = self.args.nocat,
page = self.args.page
}
end
function MessageBox:export()
local root = mw.html.create()
-- Create the box table.
local boxTable = root:tag('table')
boxTable:attr('id', self.id or nil)
for i, class in ipairs(self.classes or {}) do
boxTable:addClass(class or nil)
end
boxTable
:cssText(self.style or nil)
:attr('role', 'presentation')
if self.attrs then
boxTable:attr(self.attrs)
end
-- Add the left-hand image.
local row = boxTable:tag('tr')
if self.imageLeft then
local imageLeftCell = row:tag('td'):addClass('mbox-image')
if self.imageCellDiv then
-- If we are using a div, redefine imageLeftCell so that the image
-- is inside it. Divs use style="width: 52px;", which limits the
-- image width to 52px. If any images in a div are wider than that,
-- they may overlap with the text or cause other display problems.
imageLeftCell = imageLeftCell:tag('div'):css('width', '52px')
end
imageLeftCell:wikitext(self.imageLeft or nil)
elseif self.imageEmptyCell then
-- Some message boxes define an empty cell if no image is specified, and
-- some don't. The old template code in templates where empty cells are
-- specified gives the following hint: "No image. Cell with some width
-- or padding necessary for text cell to have 100% width."
row:tag('td')
:addClass('mbox-empty-cell')
:cssText(self.imageEmptyCellStyle or nil)
end
-- Add the text.
local textCell = row:tag('td'):addClass('mbox-text')
if self.useCollapsibleTextFields then
-- The message box uses advanced text parameters that allow things to be
-- collapsible. At the moment, only ambox uses this.
textCell:cssText(self.textstyle or nil)
local textCellDiv = textCell:tag('div')
textCellDiv
:addClass('mbox-text-span')
:wikitext(self.issue or nil)
if (self.talk or self.fix) and not self.isSmall then
textCellDiv:tag('span')
:addClass('hide-when-compact')
:wikitext(self.talk and (' ' .. self.talk) or nil)
:wikitext(self.fix and (' ' .. self.fix) or nil)
end
textCellDiv:wikitext(self.date and (' ' .. self.date) or nil)
if self.info and not self.isSmall then
textCellDiv
:tag('span')
:addClass('hide-when-compact')
:wikitext(self.info and (' ' .. self.info) or nil)
end
else
-- Default text formatting - anything goes.
textCell
:cssText(self.textstyle or nil)
:wikitext(self.text or nil)
end
-- Add the right-hand image.
if self.imageRight then
local imageRightCell = row:tag('td'):addClass('mbox-imageright')
if self.imageCellDiv then
-- If we are using a div, redefine imageRightCell so that the image
-- is inside it.
imageRightCell = imageRightCell:tag('div'):css('width', '52px')
end
imageRightCell
:wikitext(self.imageRight or nil)
end
-- Add the below row.
if self.below then
boxTable:tag('tr')
:tag('td')
:attr('colspan', self.imageRight and '3' or '2')
:addClass('mbox-text')
:cssText(self.textstyle or nil)
:wikitext(self.below or nil)
end
-- Add error message for invalid type parameters.
if self.invalidTypeError then
root:tag('div')
:css('text-align', 'center')
:wikitext(string.format(
'This message box is using an invalid "type=%s" parameter and needs fixing.',
self.type or ''
))
end
-- Add categories.
root:wikitext(self:renderCategories() or nil)
return tostring(root)
end
--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------
local p, mt = {}, {}
function p._exportClasses()
-- For testing.
return {
MessageBox = MessageBox
}
end
function p.main(boxType, args, cfgTables)
local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE))
box:setParameters()
box:setCategories()
return box:export()
end
local function templatestyles(frame, src)
return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
.. 'CONFIG_MODULE'
end
function mt.__index(t, k)
return function (frame)
if not getArgs then
getArgs = require('Module:Arguments').getArgs
end
return t.main(k, getArgs(frame, {trim = false, removeBlanks = false}))
end
end
return setmetatable(p, mt)
be00cd389f9f2afcd361e5d5e33622839555cbd9
Template:Para
10
58
108
2022-10-21T19:52:33Z
dev>Pppery
0
wikitext
text/x-wiki
<code class="tpl-para" style="word-break:break-word;{{SAFESUBST:<noinclude />#if:{{{plain|}}}|border: none; background-color: inherit;}} {{SAFESUBST:<noinclude />#if:{{{style|}}}|{{{style}}}}}">|{{SAFESUBST:<noinclude />#if:{{{1|}}}|{{{1}}}=}}{{{2|}}}</code><noinclude>
{{Documentation}}
<!--Categories and interwikis go near the bottom of the /doc subpage.-->
</noinclude>
7be5bee75307eae9342bbb9ff3a613e93e93d5a7
Module:Message box/configuration
828
91
174
2022-10-21T22:38:02Z
dev>Pppery
0
Scribunto
text/plain
--------------------------------------------------------------------------------
-- Message box configuration --
-- --
-- This module contains configuration data for [[Module:Message box]]. --
--------------------------------------------------------------------------------
return {
ambox = {
types = {
speedy = {
class = 'ambox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'ambox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'ambox-content',
image = 'Ambox important.svg'
},
style = {
class = 'ambox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'ambox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'ambox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'ambox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
allowBlankParams = {'talk', 'sect', 'date', 'issue', 'fix', 'hidden'},
allowSmall = true,
smallParam = 'left',
smallClass = 'mbox-small-left',
classes = {'metadata', 'ambox'},
imageEmptyCell = true,
imageCheckBlank = true,
imageSmallSize = '20x20px',
imageCellDiv = true,
useCollapsibleTextFields = true,
imageRightNone = true,
sectionDefault = 'article',
allowMainspaceCategories = true,
templateCategory = 'Article message templates',
templateCategoryRequireName = true,
templateErrorCategory = 'Article message templates with missing parameters',
templateErrorParamsToCheck = {'issue', 'fix'},
},
cmbox = {
types = {
speedy = {
class = 'cmbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'cmbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'cmbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'cmbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'cmbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'cmbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'cmbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'cmbox'},
imageEmptyCell = true
},
fmbox = {
types = {
warning = {
class = 'fmbox-warning',
image = 'Ambox warning pn.svg'
},
editnotice = {
class = 'fmbox-editnotice',
image = 'Information icon4.svg'
},
system = {
class = 'fmbox-system',
image = 'Information icon4.svg'
}
},
default = 'system',
showInvalidTypeError = true,
classes = {'fmbox'},
imageEmptyCell = false,
imageRightNone = false
},
imbox = {
types = {
speedy = {
class = 'imbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'imbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'imbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'imbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'imbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'imbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
license = {
class = 'imbox-license licensetpl',
image = 'Imbox license.png' -- @todo We need an SVG version of this
},
featured = {
class = 'imbox-featured',
image = 'Cscr-featured.svg'
},
notice = {
class = 'imbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'imbox'},
imageEmptyCell = true,
below = true,
templateCategory = 'File message boxes'
},
ombox = {
types = {
speedy = {
class = 'ombox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'ombox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'ombox-content',
image = 'Ambox important.svg'
},
style = {
class = 'ombox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'ombox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'ombox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'ombox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'ombox'},
allowSmall = true,
imageEmptyCell = true,
imageRightNone = true
},
tmbox = {
types = {
speedy = {
class = 'tmbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'tmbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'tmbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'tmbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'tmbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'tmbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'tmbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'tmbox'},
allowSmall = true,
imageRightNone = true,
imageEmptyCell = true,
imageEmptyCellStyle = true,
templateCategory = 'Talk message boxes'
}
}
c6bd9191861b23e474e12b19c694335c4bc3af5f
Template:Mbox
10
71
134
2022-10-21T23:02:23Z
dev>Pppery
0
Reverted edits by [[Special:Contributions/Pppery|Pppery]] ([[User talk:Pppery|talk]]) to last revision by [[User:wikipedia>Amorymeltzer|wikipedia>Amorymeltzer]]
wikitext
text/x-wiki
{{#invoke:Message box|mbox}}<noinclude>
{{documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
c262e205f85f774a23f74119179ceea11751d68e
Template:Delete
10
50
92
2022-10-24T15:28:45Z
dev>Unknown user
0
Update tvar tags
wikitext
text/x-wiki
{{MessageBox
|Flag color=firebrick
|Border color=firebrick
|Background color=#FFEEEE
|Image=[[File:Trash Can.svg|80px]]
|Message text=<span style="line-height:2;"><span style="color:red; line-height:1.2;">'''This article is a candidate for speedy deletion because {{{1}}}. '''</span><br><span style="color:#000000;">
Deleting Reason: {{{1|No reason given}}}</span></span>
}}<noinclude>[[Category:Notice templates]]</noinclude>
<includeonly>[[Category:Candidates for deletion]]</includeonly>
<noinclude>
<languages />
<translate>
<!--T:1-->
== Usage Note ==
Add this template to any page on this wiki for which you're requesting an [[<tvar name=admin>mw:Special:MyLanguage/Manual:Administrators</tvar>|administrator]] to delete, either by adding it to the very top of the page (preferred) or by replacing the existing content with this template (also acceptable) following the format prescribed below.</translate>
<code><nowiki>{{Delete|1=</nowiki>''Your deletion reason''<nowiki>}}</nowiki></code>
<translate>
<!--T:2-->
Replace ''your deletion reason'' with one of the commonly accepted reasons for deletion below, or describe concisely ''why'' you are requesting deletion.
<!--T:3-->
If you do not specify a reason in parameter <code>1=</code>, ''no deletion reason'' will be inserted, and your request ''may'' be declined if it is not apparent why deletion is being requested.
<!--T:4-->
=== Commonly accepted reasons for deletion ===
* Vandalism
* Attack page/page created solely for harassment
* Copyright violation
* Spam
* Test page. Please either use the [[<tvar name=sb>m:Meta:Sandbox</tvar>|community sandbox]] or [[<tvar name=mypsb>Special:MyPage/sandbox</tvar>|create your personal sandbox]]
* Non-controversial housekeeping
* [[<tvar name=br>Special:BrokenRedirects</tvar>|Broken redirect]]
* [[<tvar name=dr>Special:DoubleRedirects</tvar>|Double redirect]]
* Author requests deletion, or author blanked
* Subpages with no parent page
* Talk pages with no companion page and no meaningful discussion history
* Images available as identical copies on either [[<tvar name=commons>commons:Special:MyLanguage/Main Page</tvar>|Miraheze Commons]] or [[wikimediacommons:|Wikimedia Commons]]
* [[<tvar name=uc>Special:UnusedCategories</tvar>|Empty category]]
* [[<tvar name=ut>Special:UnusedTemplates</tvar>|Unused template (including a template redirect)]] with no inlinks, transclusions, or page watchers
=== Parameter(s) === <!--T:5-->
</translate>
<templatedata>
{
"params": {
"1": {
"label": "Deletion reason",
"example": "Author requests deletion, or author blanked",
"default": "No deletion reason",
"suggested": true,
"description": "Template to add to any page requiring deletion."
}
}
}
</templatedata>
</noinclude>
ae465d4609d3bbb646339e90ae469c31ce34a9df
Template:Current time
10
78
148
2022-11-01T19:57:46Z
dev>Pppery
0
Oops
wikitext
text/x-wiki
{{#switch:{{{1}}}
|Coordinated Universal Time=Current UTC is {{CURRENTTIME}}
|UTC-1=Current time for {{{1}}} is {{utc|23}}
|UTC-2=Current time for {{{1}}} is {{utc|22}}
|UTC-2:30=Current time for {{{1}}} is {{utc|21|30}}
|UTC-3=Current time for {{{1}}} is {{utc|21}}
|UTC-3:30=Current time for {{{1}}} is {{utc|20|30}}
|UTC-4=Current time for {{{1}}} is {{utc|20}}
|UTC-5=Current time for {{{1}}} is {{utc|19}}
|UTC-6=Current time for {{{1}}} is {{utc|18}}
|UTC-7=Current time for {{{1}}} is {{utc|17}}
|UTC-8=Current time for {{{1}}} is {{utc|16}}
|UTC-9=Current time for {{{1}}} is {{utc|15}}
|UTC-9:30=Current time for {{{1}}} is {{utc|14|30}}
|UTC-10=Current time for {{{1}}} is {{utc|14}}
|UTC-11=Current time for {{{1}}} is {{utc|13}}
|UTC-12=Current time for {{{1}}} is {{utc|12}}
|UTC+0:20=Current time for {{{1}}} is {{utc|0|20}}
|UTC+0:30=Current time for {{{1}}} is {{utc|0|30}}
|UTC+1=Current time for {{{1}}} is {{utc|1}}
|UTC+2=Current time for {{{1}}} is {{utc|2}}
|UTC+3=Current time for {{{1}}} is {{utc|3}}
|UTC+3:30=Current time for {{{1}}} is {{utc|3|30}}
|UTC+4=Current time for {{{1}}} is {{utc|4}}
|UTC+4:30=Current time for {{{1}}} is {{utc|4|30}}
|UTC+4:51=Current time for {{{1}}} is {{utc|4|51}}
|UTC+5=Current time for {{{1}}} is {{utc|5}}
|UTC+5:30=Current time for {{{1}}} is {{utc|5|30}}
|UTC+5:40=Current time for {{{1}}} is {{utc|5|40}}
|UTC+5:45=Current time for {{{1}}} is {{utc|5|45}}
|UTC+6=Current time for {{{1}}} is {{utc|6}}
|UTC+6:30=Current time for {{{1}}} is {{utc|6|30}}
|UTC+7=Current time for {{{1}}} is {{utc|7}}
|UTC+7:20=Current time for {{{1}}} is {{utc|7|20}}
|UTC+7:30=Current time for {{{1}}} is {{utc|7|30}}
|UTC+8=Current time for {{{1}}} is {{utc|8}}
|UTC+8:30=Current time for {{{1}}} is {{utc|8|30}}
|UTC+8:45=Current time for {{{1}}} is {{utc|8|45}}
|UTC+9=Current time for {{{1}}} is {{utc|9}}
|UTC+9:30=Current time for {{{1}}} is {{utc|9|30}}
|UTC+10=Current time for {{{1}}} is {{utc|10}}
|UTC+10:30=Current time for {{{1}}} is {{utc|10|30}}
|UTC+11=Current time for {{{1}}} is {{utc|11}}
|UTC+11:30=Current time for {{{1}}} is {{utc|11|30}}
|UTC+12=Current time for {{{1}}} is {{utc|12}}
|UTC+12:45=Current time for {{{1}}} is {{utc|12|45}}
|UTC+13=Current time for {{{1}}} is {{utc|13}}
|UTC+13:45=Current time for {{{1}}} is {{utc|13|45}}
|UTC+14=Current time for {{{1}}} is {{utc|14}}
|#default=Current time is {{CURRENTTIME}}
}}<noinclude>{{documentation|content=Returns the current time in a given timezone (defaulting to the timezone specified in [[Special:ManageWiki/settings#mw-section-localisation]], which in turn defaults to UTC)
== Examples ==
{{tlx|current time}} -> {{current time}}
{{tlx|current time|UTC+1}} -> {{current time|UTC+1}}
{{tlx|current time|UTC-5}} -> {{current time|UTC-5}}
[[Category:Templates]]
}}</noinclude>
84d7f12dbea154240f9fa86372863cd6152dd98b
Template:Description missing
10
81
154
2022-11-01T20:03:48Z
dev>Pppery
0
Use documentation
wikitext
text/x-wiki
<div class="boilerplate metadata" id="cleanup" style="text-align: center; background: #ffe; margin: .75em 15%; padding: .5em; border: 1px solid #e3e3b0;">
This media has no '''{{ #if: {{{1|}}} | {{{1}}} | description }}''', and may be lacking other information.
<br>
Media should have a summary to inform others of the content, author, source, and date if possible. If you know or have access to such information, please add it to the image page.
</div>
<includeonly>{{#switch:{{NAMESPACE}}|{{ns:6}}=|#default={{#ifeq:{{{category|}}}|no||[[Category:Images lacking a description|{{PAGENAME}}]]}}}}</includeonly><noinclude>
{{documentation}}
</noinclude>
2b5026cefd37c307f7f2ee331289c38741f834a5
Template:Discussion top
10
76
144
2022-11-01T20:08:13Z
dev>Pppery
0
Add documentation
wikitext
text/x-wiki
<div class="boilerplate metadata discussion-archived" style="background-color: #F2F4FC; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #aaa">
:The following discussion is closed. Please do not modify it. Subsequent comments should be made in a new section.
::{{{1|}}}
----<noinclude></div>{{documentation}}</noinclude>
c8b38525e188dbfa68b0e9cdd1864ceff2ed100e
Template:Discussion bottom
10
77
146
2022-11-01T20:09:03Z
dev>Pppery
0
Add documentation
wikitext
text/x-wiki
<noinclude><div></noinclude>----
:The above discussion is preserved as an archive. Please do not modify it. Subsequent comments should be made in a new section </div><noinclude>{{documentation|Template:Discussion top/doc}}</noinclude>
80d5baa979985b3b685585611b0e954d2c1c6e10
Template:Uses TemplateStyles
10
85
162
2022-11-07T02:43:20Z
dev>Pppery
0
3 revisions imported from [[:wikipedia:Template:Uses_TemplateStyles]]: Importing this now because the Wikipedia version will fail in a non-obvious way (a problem I caught on another Miraheze wiki)
wikitext
text/x-wiki
<includeonly>{{#invoke:Uses TemplateStyles|main}}</includeonly><noinclude>
{{Uses TemplateStyles|Template:Uses TemplateStyles/example.css|nocat=true}}
{{documentation}}
<!-- Categories go on the /doc subpage and interwikis go on Wikidata. -->
</noinclude>
7e26d8f257e302bd8a3dcbe53f52741ae0884f74
Module:Uses TemplateStyles
828
86
164
2022-11-07T02:51:31Z
dev>Pppery
0
Scribunto
text/plain
-- This module implements the {{Uses TemplateStyles}} template.
local mMessageBox = require('Module:Message box')
local p = {}
function p.main(frame)
local origArgs = frame:getParent().args
local args = {}
for k, v in pairs(origArgs) do
v = v:match('^%s*(.-)%s*$')
if v ~= '' then
args[k] = v
end
end
return p._main(args)
end
function p._main(args)
return p.renderBox(args)
end
function p.renderBox(tStyles)
local boxArgs = {}
if #tStyles < 1 then
boxArgs.text = '<strong class="error">Error: no TemplateStyles specified</strong>'
else
local tStylesLinks = {}
for i, ts in ipairs(tStyles) do
local sandboxLink = nil
local tsTitle = mw.title.new(ts)
if tsTitle then
local tsSandboxTitle = mw.title.new(string.format('%s:%s/sandbox/%s', tsTitle.nsText, tsTitle.baseText, tsTitle.subpageText))
if tsSandboxTitle and tsSandboxTitle.exists then
sandboxLink = string.format(' ([[:%s|sandbox]])', tsSandboxTitle.prefixedText)
end
end
tStylesLinks[i] = string.format('[[:%s]]%s', ts, sandboxLink or '')
end
local tStylesList = mw.text.listToText(tStylesLinks)
boxArgs.text = 'This ' ..
(mw.title.getCurrentTitle():inNamespaces(828,829) and 'module' or 'template') ..
' uses [[mw:Extension:TemplateStyles|TemplateStyles]]:\n' .. tStylesList
end
boxArgs.type = 'notice'
boxArgs.small = true
boxArgs.image = '[[File:Farm-Fresh css add.svg|32px|alt=CSS]]'
return mMessageBox.main('mbox', boxArgs)
end
return p
3c7364ddaba9beb17a73b0f5256cd7fc3b3051f4
Template:Header
10
23
39
2022-12-16T04:46:16Z
dev>Pppery
0
wikitext
text/x-wiki
{| style="width: 100% !important;"
|-
| style="border-top: 4px solid #{{{topbarhex|6F6F6F}}}; background-color: #{{{bodyhex|F6F6F6}}}; padding: 10px 15px;" | {{#if:{{{shortcut|}}}| {{shortcut|{{{shortcut|uselang={{{uselang|{{CURRENTCONTENTLANGUAGE}}}}}}}}}}}}<div style="font-size:180%; text-align: left; color: {{{titlecolor|}}}">'''{{{title|{{{1|{{BASEPAGENAME}}}}}}}}'''</div>
<div style="padding-top:0.3em; padding-bottom:0.1em; font-size:100%; text-align: left; color: {{{bodycolor|}}}">{{{notes|Put some notes here!}}}</div>
|-
| style="height: 10px" |
|}
{{clear}}<noinclude>{{documentation}}[[Category:templates]]</noinclude>
03aac86137ab11bfccbcceb2de919475af2953dd
Template:Documentation/mirror
10
46
84
2022-12-16T17:01:57Z
dev>Pppery
0
wikitext
text/x-wiki
<includeonly>{{subst:msgnw:{{subst:NAMESPACE}}:{{subst:BASEPAGENAME}}}}</includeonly><noinclude>{{doc}}</noinclude>
2f3df1c981931719e821f054f3db0c67072f781e
Template:Documentation/preload-sandbox
10
48
88
2022-12-16T17:04:46Z
dev>Pppery
0
7 revisions imported from [[:wikipedia:Template:Documentation/preload-sandbox]]
wikitext
text/x-wiki
<!--
Add your experimental template code here.
--><noin<includeonly></includeonly>clude>
{{Documentation}}
</noin<includeonly></includeonly>clude>
c7b7f3f85c510513f7415b512c03742fe61ee31a
Template:Documentation/preload-testcases
10
49
90
2022-12-16T17:08:51Z
dev>Pppery
0
Created page with "This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude>"
wikitext
text/x-wiki
This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude>
9e9c7d43373bd7aeaee3293842ae54db0257e549
Template:Documentation/preload
10
47
86
2022-12-16T17:09:58Z
dev>Pppery
0
wikitext
text/x-wiki
{{Documentation subpage}}
== Usage ==
<include<includeonly></includeonly>only>
<!-- Categories below this line -->
}}</include<includeonly></includeonly>only><noinclude>
{{Documentation|content=
This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template documentation page.
[[Category:Documentation preloads]]
}}</noinclude>
587413cafd960a3f7f3c9257a4160f3654fdbe0f
Template:See also
10
56
104
2023-01-10T01:56:06Z
dev>Pppery
0
Revert to version by Wikipedia->bkonrad
wikitext
text/x-wiki
{{hatnote|extraclasses=boilerplate seealso|{{{altphrase|See also}}}: {{#if:{{{1<includeonly>|</includeonly>}}} |<!--then:-->[[:{{{1}}}{{#if:{{{label 1|{{{l1|}}}}}}|{{!}}{{{label 1|{{{l1}}}}}}}}]] |<!--else:-->'''Error: [[Template:See also|Template must be given at least one article name]]'''
}}{{#if:{{{2|}}}|{{#if:{{{3|}}}|, | and }} [[:{{{2}}}{{#if:{{{label 2|{{{l2|}}}}}}|{{!}}{{{label 2|{{{l2}}}}}}}}]]
}}{{#if:{{{3|}}}|{{#if:{{{4|}}}|, |, and }} [[:{{{3}}}{{#if:{{{label 3|{{{l3|}}}}}}|{{!}}{{{label 3|{{{l3}}}}}}}}]]
}}{{#if:{{{4|}}}|{{#if:{{{5|}}}|, |, and }} [[:{{{4}}}{{#if:{{{label 4|{{{l4|}}}}}}|{{!}}{{{label 4|{{{l4}}}}}}}}]]
}}{{#if:{{{5|}}}|{{#if:{{{6|}}}|, |, and }} [[:{{{5}}}{{#if:{{{label 5|{{{l5|}}}}}}|{{!}}{{{label 5|{{{l5}}}}}}}}]]
}}{{#if:{{{6|}}}|{{#if:{{{7|}}}|, |, and }} [[:{{{6}}}{{#if:{{{label 6|{{{l6|}}}}}}|{{!}}{{{label 6|{{{l6}}}}}}}}]]
}}{{#if:{{{7|}}}|{{#if:{{{8|}}}|, |, and }} [[:{{{7}}}{{#if:{{{label 7|{{{l7|}}}}}}|{{!}}{{{label 7|{{{l7}}}}}}}}]]
}}{{#if:{{{8|}}}|{{#if:{{{9|}}}|, |, and }} [[:{{{8}}}{{#if:{{{label 8|{{{l8|}}}}}}|{{!}}{{{label 8|{{{l8}}}}}}}}]]
}}{{#if:{{{9|}}}|{{#if:{{{10|}}}|, |, and }} [[:{{{9}}}{{#if:{{{label 9|{{{l9|}}}}}}|{{!}}{{{label 9|{{{l9}}}}}}}}]]
}}{{#if:{{{10|}}}|{{#if:{{{11|}}}|, |, and }} [[:{{{10}}}{{#if:{{{label 10|{{{l10|}}}}}}|{{!}}{{{label 10|{{{l10}}}}}}}}]]
}}{{#if:{{{11|}}}|{{#if:{{{12|}}}|, |, and }} [[:{{{11}}}{{#if:{{{label 11|{{{l11|}}}}}}|{{!}}{{{label 11|{{{l11}}}}}}}}]]
}}{{#if:{{{12|}}}|{{#if:{{{13|}}}|, |, and }} [[:{{{12}}}{{#if:{{{label 12|{{{l12|}}}}}}|{{!}}{{{label 12|{{{l12}}}}}}}}]]
}}{{#if:{{{13|}}}|{{#if:{{{14|}}}|, |, and }} [[:{{{13}}}{{#if:{{{label 13|{{{l13|}}}}}}|{{!}}{{{label 13|{{{l13}}}}}}}}]]
}}{{#if:{{{14|}}}|{{#if:{{{15|}}}|, |, and }} [[:{{{14}}}{{#if:{{{label 14|{{{l14|}}}}}}|{{!}}{{{label 14|{{{l14}}}}}}}}]]
}}{{#if:{{{15|}}}|, and [[:{{{15}}}{{#if:{{{label 15|{{{l15|}}} }}}|{{!}}{{{label 15|{{{l15|}}} }}} }}]]
}}{{#if:{{{16|}}}| — '''<br/>Error: [[Template:See also|Too many links specified (maximum is 15)]]'''
}}}}<noinclude>
{{documentation}}
</noinclude>
0315f43d7e4b679054955c7a50fe554ab1df63de
Module:TNT
828
92
176
2023-01-10T02:02:55Z
dev>Pppery
0
Scribunto
text/plain
--
-- INTRO: (!!! DO NOT RENAME THIS PAGE !!!)
-- This module allows any template or module to be copy/pasted between
-- wikis without any translation changes. All translation text is stored
-- in the global Data:*.tab pages on Commons, and used everywhere.
--
-- SEE: https://www.mediawiki.org/wiki/Multilingual_Templates_and_Modules
--
-- ATTENTION:
-- Please do NOT rename this module - it has to be identical on all wikis.
-- This code is maintained at https://www.mediawiki.org/wiki/Module:TNT
-- Please do not modify it anywhere else, as it may get copied and override your changes.
-- Suggestions can be made at https://www.mediawiki.org/wiki/Module_talk:TNT
--
-- DESCRIPTION:
-- The "msg" function uses a Commons dataset to translate a message
-- with a given key (e.g. source-table), plus optional arguments
-- to the wiki markup in the current content language.
-- Use lang=xx to set language. Example:
--
-- {{#invoke:TNT | msg
-- | I18n/Template:Graphs.tab <!-- https://commons.wikimedia.org/wiki/Data:I18n/Template:Graphs.tab -->
-- | source-table <!-- uses a translation message with id = "source-table" -->
-- | param1 }} <!-- optional parameter -->
--
--
-- The "doc" function will generate the <templatedata> parameter documentation for templates.
-- This way all template parameters can be stored and localized in a single Commons dataset.
-- NOTE: "doc" assumes that all documentation is located in Data:Templatedata/* on Commons.
--
-- {{#invoke:TNT | doc | Graph:Lines }}
-- uses https://commons.wikimedia.org/wiki/Data:Templatedata/Graph:Lines.tab
-- if the current page is Template:Graph:Lines/doc
--
local p = {}
local i18nDataset = 'I18n/Module:TNT.tab'
-- Forward declaration of the local functions
local sanitizeDataset, loadData, link, formatMessage
function p.msg(frame)
local dataset, id
local params = {}
local lang = nil
for k, v in pairs(frame.args) do
if k == 1 then
dataset = mw.text.trim(v)
elseif k == 2 then
id = mw.text.trim(v)
elseif type(k) == 'number' then
params[k - 2] = mw.text.trim(v)
elseif k == 'lang' and v ~= '_' then
lang = mw.text.trim(v)
end
end
return formatMessage(dataset, id, params, lang)
end
-- Identical to p.msg() above, but used from other lua modules
-- Parameters: name of dataset, message key, optional arguments
-- Example with 2 params: format('I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset')
function p.format(dataset, key, ...)
local checkType = require('libraryUtil').checkType
checkType('format', 1, dataset, 'string')
checkType('format', 2, key, 'string')
return formatMessage(dataset, key, {...})
end
-- Identical to p.msg() above, but used from other lua modules with the language param
-- Parameters: language code, name of dataset, message key, optional arguments
-- Example with 2 params: formatInLanguage('es', I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset')
function p.formatInLanguage(lang, dataset, key, ...)
local checkType = require('libraryUtil').checkType
checkType('formatInLanguage', 1, lang, 'string')
checkType('formatInLanguage', 2, dataset, 'string')
checkType('formatInLanguage', 3, key, 'string')
return formatMessage(dataset, key, {...}, lang)
end
-- Obsolete function that adds a 'c:' prefix to the first param.
-- "Sandbox/Sample.tab" -> 'c:Data:Sandbox/Sample.tab'
function p.link(frame)
return link(frame.args[1])
end
function p.doc(frame)
local dataset = 'Templatedata/' .. sanitizeDataset(frame.args[1])
return frame:extensionTag('templatedata', p.getTemplateData(dataset)) ..
formatMessage(i18nDataset, 'edit_doc', {link(dataset)})
end
function p.getTemplateData(dataset)
-- TODO: add '_' parameter once lua starts reindexing properly for "all" languages
local data = loadData(dataset)
local names = {}
for _, field in ipairs(data.schema.fields) do
table.insert(names, field.name)
end
local params = {}
local paramOrder = {}
for _, row in ipairs(data.data) do
local newVal = {}
local name = nil
for pos, columnName in ipairs(names) do
if columnName == 'name' then
name = row[pos]
else
newVal[columnName] = row[pos]
end
end
if name then
params[name] = newVal
table.insert(paramOrder, name)
end
end
-- Work around json encoding treating {"1":{...}} as an [{...}]
params['zzz123']=''
local json = mw.text.jsonEncode({
params=params,
paramOrder=paramOrder,
description=data.description
})
json = string.gsub(json,'"zzz123":"",?', "")
return json
end
-- Local functions
sanitizeDataset = function(dataset)
if not dataset then
return nil
end
dataset = mw.text.trim(dataset)
if dataset == '' then
return nil
elseif string.sub(dataset,-4) ~= '.tab' then
return dataset .. '.tab'
else
return dataset
end
end
loadData = function(dataset, lang)
dataset = sanitizeDataset(dataset)
if not dataset then
error(formatMessage(i18nDataset, 'error_no_dataset', {}))
end
-- Give helpful error to thirdparties who try and copy this module.
if not mw.ext or not mw.ext.data or not mw.ext.data.get then
error(string.format([['''Missing JsonConfig extension, or not properly configured;
Cannot load https://commons.wikimedia.org/wiki/Data:%s. Please properly enable the JSONConfig extension at Special:ManageWiki/extensions#mw-prefsection-other
See https://www.mediawiki.org/wiki/Extension:JsonConfig#Supporting_Wikimedia_templates''']], dataset))
end
local data = mw.ext.data.get(dataset, lang)
if data == false then
if dataset == i18nDataset then
-- Prevent cyclical calls
error('Missing Commons dataset ' .. i18nDataset)
else
error(formatMessage(i18nDataset, 'error_bad_dataset', {link(dataset)}))
end
end
return data
end
-- Given a dataset name, convert it to a title with the 'commons:data:' prefix
link = function(dataset)
return 'c:Data:' .. mw.text.trim(dataset or '')
end
formatMessage = function(dataset, key, params, lang)
for _, row in pairs(loadData(dataset, lang).data) do
local id, msg = unpack(row)
if id == key then
local result = mw.message.newRawMessage(msg, unpack(params or {}))
return result:plain()
end
end
if dataset == i18nDataset then
-- Prevent cyclical calls
error('Invalid message key "' .. key .. '"')
else
error(formatMessage(i18nDataset, 'error_bad_msgkey', {key, link(dataset)}))
end
end
return p
6d981852d69d5958a60d96d24c311680564c6103
Template:Pending
10
39
70
2023-01-16T07:23:51Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
{{On hold|{{{1|Pending}}}}}<noinclude>{{Documentation}}</noinclude>
3d534f8f2cf14f73be843d306efcecbff05c7f5e
Template:Endorse
10
31
55
2023-01-16T07:24:17Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
[[File:Symbol support2 vote.svg|link=|alt=|16px|]] '''{{{1|Endorse}}}'''<noinclude>{{Documentation}}</noinclude>
23cc6c948818ca6949bd9af0991af58f1858483f
Template:Partly done
10
38
68
2023-01-16T07:26:51Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
<span class="nowrap">[[File:Yellow_check.svg|18px|link=|alt=]] '''{{{1|Partly done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
24a90b5a5c4c716b7ec12889fbd09a1da2ba1ca3
Template:Resolved
10
41
74
2023-01-16T07:28:00Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
<span class="nowrap">[[File:Yes check.svg|18px|link=]] '''{{{1|Resolved}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
bcebb832c81fc395e8891f82747510f76292cb34
Template:Agree
10
24
41
2023-01-16T07:29:51Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
[[File:Symbol confirmed.svg|18px|link=]] '''{{{1|Agree}}}'''<noinclude>{{documentation}}</noinclude>
775ddedaccda0d477a1b3c82d422e3760c862609
Template:Working
10
45
82
2023-01-16T07:30:45Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
[[File:Icon tools.svg|20px|link=]] '''{{{1|Working}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
0619210f08d5114b9a348b4f1045a0b6f4552012
Template:Idea
10
33
59
2023-01-16T07:31:31Z
dev>BrandonWM
0
from meta
wikitext
text/x-wiki
[[File:Dialog-information on.svg|18px|link=]] '''{{{1|Idea}}}:'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
e4062daed60634ce9e9cd2f052d9102bcf7e2916
Template:Reviewing
10
42
76
2023-01-16T07:34:11Z
dev>BrandonWM
0
add from meta
wikitext
text/x-wiki
[[File:Pictogram voting wait green.svg|17px|link=]] '''{{{1|Reviewing}}}...'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
0184f75a66f991d9eb99f23a75df36dd184e0c4b
Template:Note
10
30
53
2023-01-16T07:34:58Z
dev>BrandonWM
0
Created page with "[[File:Pictogram voting info.svg|18px|link=]] '''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>"
wikitext
text/x-wiki
[[File:Pictogram voting info.svg|18px|link=]] '''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
4d5cae62908f9cc8da2988712b236fe939bc80e2
Template:Question
10
40
72
2023-01-16T07:35:38Z
dev>BrandonWM
0
meta
wikitext
text/x-wiki
[[File:Pictogram voting question.svg|18px|link=]] '''{{{1|Question:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude>
9fae3d5ccc70d95a5a7de8983d7a82c1a55853e3
Template:High priority
10
32
57
2023-01-16T07:36:41Z
dev>BrandonWM
0
meta
wikitext
text/x-wiki
[[File:Exclamationdiamond.svg|20px|link=]] '''{{{1|High Priority}}}'''{{{{{|safesubst:}}}#if:{{{note|{{{reason|}}}}}}|<nowiki />: {{{note|{{{reason}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
65d49ca7f928fef46651d89d894267497560a60b
Template:Thank you
10
43
78
2023-01-16T07:37:23Z
dev>BrandonWM
0
meta
wikitext
text/x-wiki
<span class="nowrap">[[File:Face-smile.svg|18px|link=]] '''{{{1|Thank you}}}'''</span><noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
4312420b6485d1eb316af5c56f663a7d618afb9b
Template:In progress
10
34
61
2023-01-16T22:21:50Z
dev>Pppery
0
Pppery moved page [[Template:In Progress]] to [[Template:In progress]]
wikitext
text/x-wiki
[[File:Pictogram voting info.svg|16px|link=|alt=]] '''{{{1|In Progress}}}'''<noinclude>{{documentation}}</noinclude>
ff9e3ff4245b3dcc9ae45a1f8e3c7e7830fc0fff
Template:Done
10
29
51
2023-01-16T22:22:56Z
dev>Pppery
0
Reverted edits by [[Special:Contributions/BrandonWM|BrandonWM]] ([[User talk:BrandonWM|talk]]) to last revision by MacFan4000
wikitext
text/x-wiki
<span class="nowrap">[[File:Yes check.svg|18px|link=|alt=]] '''{{{1|Done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>
{{documentation}}
[[Category:Resolution templates]]</noinclude>
717c1385d516cd84dc05a10ba88359a52c9d8415
Template:Withdrawn
10
44
80
2023-01-16T22:50:23Z
dev>Pppery
0
wikitext
text/x-wiki
[[File:Cancelled process mini.svg|200x20px|link=|alt=]] '''{{{1|Request withdrawn}}}'''<noinclude>{{documentation}}</noinclude>
24c0cd218d3a61ac8b524c6f8d1b5cc405ca3d80
Template:Custom resolution
10
27
47
2023-01-16T22:51:28Z
dev>Pppery
0
wikitext
text/x-wiki
<span class="nowrap">[[File:{{{1|Cancelled process mini.svg}}}|18px|alt={{{2|Text here}}}]] <span style="{{{3|">'''{{{2|Text here}}}'''</span></span>
<noinclude>{{Documentation|content=
This template allows for the creation of custom [[Template:Template list#Resolution templates|resolution templates]] using 2 parameters.
}}[[Category:Resolution templates]]</noinclude>
a563b1f700886c4f97480a7ee81988b33af01ccf
Template:Information/style.css
10
53
98
2023-01-16T23:32:31Z
dev>Pppery
0
text
text/plain
.fileinfo-paramfield {
background: #ccf;
text-align: right;
padding-right: 0.4em;
width: 15%;
font-weight: bold;
}
/* [[Category:Template stylesheets]] */
396fcf8276bedcc9dad608bdbd9bf1be7f90424d
Module:Documentation/styles.css
828
97
185
2023-01-16T23:40:04Z
dev>Pppery
0
text
text/plain
.documentation,
.documentation-metadata {
border: 1px solid #a2a9b1;
background-color: #ecfcf4;
clear: both;
}
.documentation {
margin: 1em 0 0 0;
padding: 1em;
}
.documentation-metadata {
margin: 0.2em 0; /* same margin left-right as .documentation */
font-style: italic;
padding: 0.4em 1em; /* same padding left-right as .documentation */
}
.documentation-startbox {
padding-bottom: 3px;
border-bottom: 1px solid #aaa;
margin-bottom: 1ex;
}
.documentation-heading {
font-weight: bold;
font-size: 125%;
}
.documentation-clear { /* Don't want things to stick out where they shouldn't. */
clear: both;
}
.documentation-toolbar {
font-style: normal;
font-size: 85%;
}
/* [[Category:Template stylesheets]] */
5fb984fe8632dc068db16853a824c9f3d5175dd9
Module:String
828
93
178
2023-01-20T02:43:41Z
dev>Pppery
0
And this one too
Scribunto
text/plain
--[[
This module is intended to provide access to basic string functions.
Most of the functions provided here can be invoked with named parameters,
unnamed parameters, or a mixture. If named parameters are used, Mediawiki will
automatically remove any leading or trailing whitespace from the parameter.
Depending on the intended use, it may be advantageous to either preserve or
remove such whitespace.
Global options
ignore_errors: If set to 'true' or 1, any error condition will result in
an empty string being returned rather than an error message.
error_category: If an error occurs, specifies the name of a category to
include with the error message. The default category is
[Category:Errors reported by Module String].
no_category: If set to 'true' or 1, no category will be added if an error
is generated.
Unit tests for this module are available at Module:String/tests.
]]
local str = {}
--[[
len
This function returns the length of the target string.
Usage:
{{#invoke:String|len|target_string|}}
OR
{{#invoke:String|len|s=target_string}}
Parameters
s: The string whose length to report
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the target string.
]]
function str.len( frame )
local new_args = str._getParameters( frame.args, {'s'} )
local s = new_args['s'] or ''
return mw.ustring.len( s )
end
--[[
sub
This function returns a substring of the target string at specified indices.
Usage:
{{#invoke:String|sub|target_string|start_index|end_index}}
OR
{{#invoke:String|sub|s=target_string|i=start_index|j=end_index}}
Parameters
s: The string to return a subset of
i: The fist index of the substring to return, defaults to 1.
j: The last index of the string to return, defaults to the last character.
The first character of the string is assigned an index of 1. If either i or j
is a negative value, it is interpreted the same as selecting a character by
counting from the end of the string. Hence, a value of -1 is the same as
selecting the last character of the string.
If the requested indices are out of range for the given string, an error is
reported.
]]
function str.sub( frame )
local new_args = str._getParameters( frame.args, { 's', 'i', 'j' } )
local s = new_args['s'] or ''
local i = tonumber( new_args['i'] ) or 1
local j = tonumber( new_args['j'] ) or -1
local len = mw.ustring.len( s )
-- Convert negatives for range checking
if i < 0 then
i = len + i + 1
end
if j < 0 then
j = len + j + 1
end
if i > len or j > len or i < 1 or j < 1 then
return str._error( 'String subset index out of range' )
end
if j < i then
return str._error( 'String subset indices out of order' )
end
return mw.ustring.sub( s, i, j )
end
--[[
_match
This function returns a substring from the source string that matches a
specified pattern. It is exported for use in other modules
Usage:
strmatch = require("Module:String")._match
sresult = strmatch( s, pattern, start, match, plain, nomatch )
Parameters
s: The string to search
pattern: The pattern or string to find within the string
start: The index within the source string to start the search. The first
character of the string has index 1. Defaults to 1.
match: In some cases it may be possible to make multiple matches on a single
string. This specifies which match to return, where the first match is
match= 1. If a negative number is specified then a match is returned
counting from the last match. Hence match = -1 is the same as requesting
the last match. Defaults to 1.
plain: A flag indicating that the pattern should be understood as plain
text. Defaults to false.
nomatch: If no match is found, output the "nomatch" value rather than an error.
For information on constructing Lua patterns, a form of [regular expression], see:
* http://www.lua.org/manual/5.1/manual.html#5.4.1
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns
]]
-- This sub-routine is exported for use in other modules
function str._match( s, pattern, start, match_index, plain_flag, nomatch )
if s == '' then
return str._error( 'Target string is empty' )
end
if pattern == '' then
return str._error( 'Pattern string is empty' )
end
start = tonumber(start) or 1
if math.abs(start) < 1 or math.abs(start) > mw.ustring.len( s ) then
return str._error( 'Requested start is out of range' )
end
if match_index == 0 then
return str._error( 'Match index is out of range' )
end
if plain_flag then
pattern = str._escapePattern( pattern )
end
local result
if match_index == 1 then
-- Find first match is simple case
result = mw.ustring.match( s, pattern, start )
else
if start > 1 then
s = mw.ustring.sub( s, start )
end
local iterator = mw.ustring.gmatch(s, pattern)
if match_index > 0 then
-- Forward search
for w in iterator do
match_index = match_index - 1
if match_index == 0 then
result = w
break
end
end
else
-- Reverse search
local result_table = {}
local count = 1
for w in iterator do
result_table[count] = w
count = count + 1
end
result = result_table[ count + match_index ]
end
end
if result == nil then
if nomatch == nil then
return str._error( 'Match not found' )
else
return nomatch
end
else
return result
end
end
--[[
match
This function returns a substring from the source string that matches a
specified pattern.
Usage:
{{#invoke:String|match|source_string|pattern_string|start_index|match_number|plain_flag|nomatch_output}}
OR
{{#invoke:String|match|s=source_string|pattern=pattern_string|start=start_index
|match=match_number|plain=plain_flag|nomatch=nomatch_output}}
Parameters
s: The string to search
pattern: The pattern or string to find within the string
start: The index within the source string to start the search. The first
character of the string has index 1. Defaults to 1.
match: In some cases it may be possible to make multiple matches on a single
string. This specifies which match to return, where the first match is
match= 1. If a negative number is specified then a match is returned
counting from the last match. Hence match = -1 is the same as requesting
the last match. Defaults to 1.
plain: A flag indicating that the pattern should be understood as plain
text. Defaults to false.
nomatch: If no match is found, output the "nomatch" value rather than an error.
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from each string. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
If the match_number or start_index are out of range for the string being queried, then
this function generates an error. An error is also generated if no match is found.
If one adds the parameter ignore_errors=true, then the error will be suppressed and
an empty string will be returned on any failure.
For information on constructing Lua patterns, a form of [regular expression], see:
* http://www.lua.org/manual/5.1/manual.html#5.4.1
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns
]]
-- This is the entry point for #invoke:String|match
function str.match( frame )
local new_args = str._getParameters( frame.args, {'s', 'pattern', 'start', 'match', 'plain', 'nomatch'} )
local s = new_args['s'] or ''
local start = tonumber( new_args['start'] ) or 1
local plain_flag = str._getBoolean( new_args['plain'] or false )
local pattern = new_args['pattern'] or ''
local match_index = math.floor( tonumber(new_args['match']) or 1 )
local nomatch = new_args['nomatch']
return str._match( s, pattern, start, match_index, plain_flag, nomatch )
end
--[[
pos
This function returns a single character from the target string at position pos.
Usage:
{{#invoke:String|pos|target_string|index_value}}
OR
{{#invoke:String|pos|target=target_string|pos=index_value}}
Parameters
target: The string to search
pos: The index for the character to return
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the target string. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
The first character has an index value of 1.
If one requests a negative value, this function will select a character by counting backwards
from the end of the string. In other words pos = -1 is the same as asking for the last character.
A requested value of zero, or a value greater than the length of the string returns an error.
]]
function str.pos( frame )
local new_args = str._getParameters( frame.args, {'target', 'pos'} )
local target_str = new_args['target'] or ''
local pos = tonumber( new_args['pos'] ) or 0
if pos == 0 or math.abs(pos) > mw.ustring.len( target_str ) then
return str._error( 'String index out of range' )
end
return mw.ustring.sub( target_str, pos, pos )
end
--[[
find
This function allows one to search for a target string or pattern within another
string.
Usage:
{{#invoke:String|find|source_str|target_string|start_index|plain_flag}}
OR
{{#invoke:String|find|source=source_str|target=target_str|start=start_index|plain=plain_flag}}
Parameters
source: The string to search
target: The string or pattern to find within source
start: The index within the source string to start the search, defaults to 1
plain: Boolean flag indicating that target should be understood as plain
text and not as a Lua style regular expression, defaults to true
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the parameter. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
This function returns the first index >= "start" where "target" can be found
within "source". Indices are 1-based. If "target" is not found, then this
function returns 0. If either "source" or "target" are missing / empty, this
function also returns 0.
This function should be safe for UTF-8 strings.
]]
function str.find( frame )
local new_args = str._getParameters( frame.args, {'source', 'target', 'start', 'plain' } )
local source_str = new_args['source'] or ''
local pattern = new_args['target'] or ''
local start_pos = tonumber(new_args['start']) or 1
local plain = new_args['plain'] or true
if source_str == '' or pattern == '' then
return 0
end
plain = str._getBoolean( plain )
local start = mw.ustring.find( source_str, pattern, start_pos, plain )
if start == nil then
start = 0
end
return start
end
--[[
replace
This function allows one to replace a target string or pattern within another
string.
Usage:
{{#invoke:String|replace|source_str|pattern_string|replace_string|replacement_count|plain_flag}}
OR
{{#invoke:String|replace|source=source_string|pattern=pattern_string|replace=replace_string|
count=replacement_count|plain=plain_flag}}
Parameters
source: The string to search
pattern: The string or pattern to find within source
replace: The replacement text
count: The number of occurences to replace, defaults to all.
plain: Boolean flag indicating that pattern should be understood as plain
text and not as a Lua style regular expression, defaults to true
]]
function str.replace( frame )
local new_args = str._getParameters( frame.args, {'source', 'pattern', 'replace', 'count', 'plain' } )
local source_str = new_args['source'] or ''
local pattern = new_args['pattern'] or ''
local replace = new_args['replace'] or ''
local count = tonumber( new_args['count'] )
local plain = new_args['plain'] or true
if source_str == '' or pattern == '' then
return source_str
end
plain = str._getBoolean( plain )
if plain then
pattern = str._escapePattern( pattern )
replace = mw.ustring.gsub( replace, "%%", "%%%%" ) --Only need to escape replacement sequences.
end
local result
if count ~= nil then
result = mw.ustring.gsub( source_str, pattern, replace, count )
else
result = mw.ustring.gsub( source_str, pattern, replace )
end
return result
end
--[[
simple function to pipe string.rep to templates.
]]
function str.rep( frame )
local repetitions = tonumber( frame.args[2] )
if not repetitions then
return str._error( 'function rep expects a number as second parameter, received "' .. ( frame.args[2] or '' ) .. '"' )
end
return string.rep( frame.args[1] or '', repetitions )
end
--[[
escapePattern
This function escapes special characters from a Lua string pattern. See [1]
for details on how patterns work.
[1] https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
Usage:
{{#invoke:String|escapePattern|pattern_string}}
Parameters
pattern_string: The pattern string to escape.
]]
function str.escapePattern( frame )
local pattern_str = frame.args[1]
if not pattern_str then
return str._error( 'No pattern string specified' )
end
local result = str._escapePattern( pattern_str )
return result
end
--[[
count
This function counts the number of occurrences of one string in another.
]]
function str.count(frame)
local args = str._getParameters(frame.args, {'source', 'pattern', 'plain'})
local source = args.source or ''
local pattern = args.pattern or ''
local plain = str._getBoolean(args.plain or true)
if plain then
pattern = str._escapePattern(pattern)
end
local _, count = mw.ustring.gsub(source, pattern, '')
return count
end
--[[
endswith
This function determines whether a string ends with another string.
]]
function str.endswith(frame)
local args = str._getParameters(frame.args, {'source', 'pattern'})
local source = args.source or ''
local pattern = args.pattern or ''
if pattern == '' then
-- All strings end with the empty string.
return "yes"
end
if mw.ustring.sub(source, -mw.ustring.len(pattern), -1) == pattern then
return "yes"
else
return ""
end
end
--[[
join
Join all non empty arguments together; the first argument is the separator.
Usage:
{{#invoke:String|join|sep|one|two|three}}
]]
function str.join(frame)
local args = {}
local sep
for _, v in ipairs( frame.args ) do
if sep then
if v ~= '' then
table.insert(args, v)
end
else
sep = v
end
end
return table.concat( args, sep or '' )
end
--[[
Helper function that populates the argument list given that user may need to use a mix of
named and unnamed parameters. This is relevant because named parameters are not
identical to unnamed parameters due to string trimming, and when dealing with strings
we sometimes want to either preserve or remove that whitespace depending on the application.
]]
function str._getParameters( frame_args, arg_list )
local new_args = {}
local index = 1
local value
for _, arg in ipairs( arg_list ) do
value = frame_args[arg]
if value == nil then
value = frame_args[index]
index = index + 1
end
new_args[arg] = value
end
return new_args
end
--[[
Helper function to handle error messages.
]]
function str._error( error_str )
local frame = mw.getCurrentFrame()
local error_category = frame.args.error_category or 'Errors reported by Module String'
local ignore_errors = frame.args.ignore_errors or false
local no_category = frame.args.no_category or false
if str._getBoolean(ignore_errors) then
return ''
end
local error_str = '<strong class="error">String Module Error: ' .. error_str .. '</strong>'
if error_category ~= '' and not str._getBoolean( no_category ) then
error_str = '[[Category:' .. error_category .. ']]' .. error_str
end
return error_str
end
--[[
Helper Function to interpret boolean strings
]]
function str._getBoolean( boolean_str )
local boolean_value
if type( boolean_str ) == 'string' then
boolean_str = boolean_str:lower()
if boolean_str == 'false' or boolean_str == 'no' or boolean_str == '0'
or boolean_str == '' then
boolean_value = false
else
boolean_value = true
end
elseif type( boolean_str ) == 'boolean' then
boolean_value = boolean_str
else
error( 'No boolean value found' )
end
return boolean_value
end
--[[
Helper function that escapes all pattern characters so that they will be treated
as plain text.
]]
function str._escapePattern( pattern_str )
return mw.ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" )
end
return str
73c9d229ca32cb5e05a3873238b69fec347cf4b1
Template:Soft redirect
10
55
102
2023-01-20T02:48:04Z
dev>Pppery
0
Drop categorization ([[customizing templattes|an assumption about the structure of the wiki]]), and clean up
wikitext
text/x-wiki
__NONEWSECTIONLINK__[[File:Softredirarrow.svg|64px|Soft redirect to:|link=]]<span class="redirectText" id="softredirect">[[:{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}|{{{2|{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}}}}]]</span><br /><span style="font-size:85%; padding-left:48px;">This page is a [[metawikimedia:soft redirect|soft redirect]].</span><noinclude>
{{Documentation}}
</noinclude>
a965c0fe43aa0fe8f0e17ed40d725f0e7b3649f6
MediaWiki:Twinkle.js
8
84
160
2023-02-01T23:54:44Z
dev>Pppery
0
javascript
text/javascript
/* twinkle js file [[Category:Twinkle]]
loads all dependencies then twinkle */
mw.loader.getScript( 'https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript' ).then( function () {mw.loader.load(["mediawiki.util"]);mw.loader.load(["jquery.ui"]);mw.loader.load(["jquery.tipsy"]);mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.css?action=raw&ctype=text/css", "text/css");mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript");}, function ( e ) {mw.log.error( e.message );} );
a49ff2e963c82cee4e45c90883102d1e662bd8b8
MediaWiki:Gadget-Twinkle.js
8
83
158
2023-02-03T01:06:43Z
dev>Pppery
0
Reverted edits by [[Special:Contributions/Pppery|Pppery]] ([[User talk:Pppery|talk]]) to last revision by Naleksuh
javascript
text/javascript
/**
Twinkle.js [[Category:Twinkle]]
Forked from simplewiki's version of Twinkle and de-Wikipedia-fied by Naleksuh
Currently in beta and still has some reseblences to Wikipedia. Will be more fine-tuned over time
*/
//<nowiki>
mw.loader.load("https://dev.miraheze.org/w/index.php?title=MediaWiki:Jquerymigrate-3.3.2.js&action=raw&ctype=text/javascript");
( function ( window, document, $, undefined ) { // Wrap with anonymous function
var Twinkle = {};
window.Twinkle = Twinkle; // allow global access
// for use by custom modules (normally empty)
Twinkle.initCallbacks = [];
Twinkle.addInitCallback = function twinkleAddInitCallback( func ) {
Twinkle.initCallbacks.push( func );
};
Twinkle.defaultConfig = {};
/**
* Twinkle.defaultConfig.twinkle and Twinkle.defaultConfig.friendly
*
* This holds the default set of preferences used by Twinkle. (The |friendly| object holds preferences stored in the FriendlyConfig object.)
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig.twinkle = {
// General
summaryAd: " ([[mh:dev:Twinkle|TW]])",
deletionSummaryAd: " ([[mh:dev:Twinkle|TW]])",
protectionSummaryAd: " ([[mh:dev:Twinkle|TW]])",
userTalkPageMode: "window",
dialogLargeFont: false,
// Fluff (revert and rollback)
openTalkPage: [ "agf", "norm", "vand" ],
openTalkPageOnAutoRevert: false,
markRevertedPagesAsMinor: [ "vand" ],
watchRevertedPages: [ "agf", "norm", "vand", "torev" ],
offerReasonOnNormalRevert: true,
confirmOnFluff: false,
showRollbackLinks: [ "diff", "others" ],
// CSD
speedySelectionStyle: "buttonClick",
speedyPromptOnG7: false,
watchSpeedyPages: [ "g3", "g5", "g10", "g11", "g12" ],
markSpeedyPagesAsPatrolled: true,
// these next two should probably be identical by default
notifyUserOnSpeedyDeletionNomination: [ ],
welcomeUserOnSpeedyDeletionNotification: [ ],
promptForSpeedyDeletionSummary: [ "db" ],
openUserTalkPageOnSpeedyDelete: [ "db"],
deleteTalkPageOnDelete: false,
deleteSysopDefaultToTag: false,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: "Deletion request log",
noLogOnSpeedyNomination: [ "u1" ],
// Unlink
unlinkNamespaces: [ "0" ],
// Warn
defaultWarningGroup: "1",
showSharedIPNotice: true,
watchWarnings: true,
blankTalkpageOnIndefBlock: false,
// XfD
xfdWatchDiscussion: "default",
xfdWatchList: "no",
xfdWatchPage: "default",
xfdWatchUser: "default",
// Hidden preferences
revertMaxRevisions: 50,
batchdeleteChunks: 50,
batchDeleteMinCutOff: 5,
batchMax: 5000,
batchProtectChunks: 50,
batchProtectMinCutOff: 5,
batchundeleteChunks: 50,
batchUndeleteMinCutOff: 5
};
// now some skin dependent config.
if ( mw.config.get( "skin" ) === "vector" || mw.config.get("skin") === "vector-2022") {
Twinkle.defaultConfig.twinkle.portletArea = "right-navigation";
Twinkle.defaultConfig.twinkle.portletId = "p-twinkle";
Twinkle.defaultConfig.twinkle.portletName = "TW";
Twinkle.defaultConfig.twinkle.portletType = "menu";
Twinkle.defaultConfig.twinkle.portletNext = "p-search";
} else {
Twinkle.defaultConfig.twinkle.portletArea = null;
Twinkle.defaultConfig.twinkle.portletId = "p-cactions";
Twinkle.defaultConfig.twinkle.portletName = null;
Twinkle.defaultConfig.twinkle.portletType = null;
Twinkle.defaultConfig.twinkle.portletNext = null;
}
Twinkle.defaultConfig.friendly = {
// Tag
groupByDefault: true,
watchTaggedPages: true,
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: true,
tagArticleSortOrder: "cat",
customTagList: [],
// Stub
watchStubbedPages: true,
markStubbedPagesAsMinor: false,
markStubbedPagesAsPatrolled: true,
stubArticleSortOrder: "cat",
// Welcome
topWelcomes: false,
watchWelcomes: true,
welcomeHeading: "Welcome",
insertHeadings: true,
insertUsername: true,
insertSignature: true, // sign welcome templates, where appropriate
quickWelcomeMode: "norm",
quickWelcomeTemplate: "welcome",
customWelcomeList: [],
// Talkback
markTalkbackAsMinor: true,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: "Talkback",
adminNoticeHeading: "Notice",
mailHeading: "You've got mail!",
// Shared
markSharedIPAsMinor: true
};
Twinkle.getPref = function twinkleGetPref( name ) {
var result;
if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.twinkle === "object" ) {
// look in Twinkle.prefs (twinkleoptions.js)
result = Twinkle.prefs.twinkle[name];
} else if ( typeof window.TwinkleConfig === "object" ) {
// look in TwinkleConfig
result = window.TwinkleConfig[name];
}
if ( result === undefined ) {
return Twinkle.defaultConfig.twinkle[name];
}
return result;
};
Twinkle.getFriendlyPref = function twinkleGetFriendlyPref(name) {
var result;
if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.friendly === "object" ) {
// look in Twinkle.prefs (twinkleoptions.js)
result = Twinkle.prefs.friendly[ name ];
} else if ( typeof window.FriendlyConfig === "object" ) {
// look in FriendlyConfig
result = window.FriendlyConfig[ name ];
}
if ( result === undefined ) {
return Twinkle.defaultConfig.friendly[ name ];
}
return result;
};
/**
* **************** twAddPortlet() ****************
*
* Adds a portlet menu to one of the navigation areas on the page.
* This is necessarily quite a hack since skins, navigation areas, and
* portlet menu types all work slightly different.
*
* Available navigation areas depend on the skin used.
* Vector:
* For each option, the outer div class contains "vector-menu", the inner div class is "vector-menu-content", and the ul is "vector-menu-content-list"
* "mw-panel", outer div class contains "vector-menu-portal". Existing portlets/elements: "p-logo", "p-navigation", "p-interaction", "p-tb", "p-coll-print_export"
* "left-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-namespaces", "p-variants" (menu)
* "right-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-views", "p-cactions" (menu), "p-search"
* Special layout of p-personal portlet (part of "head") through specialized styles.
* Monobook:
* "column-one", outer div class "portlet", inner div class "pBody". Existing portlets: "p-cactions", "p-personal", "p-logo", "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export"
* Special layout of p-cactions and p-personal through specialized styles.
* Modern:
* "mw_contentwrapper" (top nav), outer div class "portlet", inner div class "pBody". Existing portlets or elements: "p-cactions", "mw_content"
* "mw_portlets" (sidebar), outer div class "portlet", inner div class "pBody". Existing portlets: "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export"
*
* @param String navigation -- id of the target navigation area (skin dependant, on vector either of "left-navigation", "right-navigation", or "mw-panel")
* @param String id -- id of the portlet menu to create, preferably start with "p-".
* @param String text -- name of the portlet menu to create. Visibility depends on the class used.
* @param String type -- type of portlet. Currently only used for the vector non-sidebar portlets, pass "menu" to make this portlet a drop down menu.
* @param Node nextnodeid -- the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end.
*
* @return Node -- the DOM node of the new item (a DIV element) or null
*/
function twAddPortlet( navigation, id, text, type, nextnodeid )
{
//sanity checks, and get required DOM nodes
var root = document.getElementById( navigation );
if ( !root ) {
return null;
}
var item = document.getElementById( id );
if ( item ) {
if ( item.parentNode && item.parentNode === root ) {
return item;
}
return null;
}
var nextnode;
if ( nextnodeid ) {
nextnode = document.getElementById(nextnodeid);
}
if ((mw.config.get('skin') !== 'vector' && mw.config.get('skin') !== 'vector-2022') || (navigation !== 'left-navigation' && navigation !== 'right-navigation')) {
type = null; // menu supported only in vector's #left-navigation & #right-navigation
}
var outerDivClass;
var innerDivClass;
switch (mw.config.get('skin'))
{
case "vector":
case 'vector-2022':
if ( navigation !== "portal" && navigation !== "left-navigation" && navigation !== "right-navigation" ) {
navigation = "mw-panel";
}
outerDivClass = 'vector-menu vector-menu-' + (navigation === 'mw-panel' ? 'portal' : type === 'menu' ? 'dropdown vector-menu-dropdown-noicon' : 'tabs');
innerDivClass = 'vector-menu-content';
break;
case "modern":
if ( navigation !== "mw_portlets" && navigation !== "mw_contentwrapper" ) {
navigation = "mw_portlets";
}
outerDivClass = "portlet";
innerDivClass = "pBody";
break;
default:
navigation = "column-one";
outerDivClass = "portlet";
innerDivClass = "pBody";
break;
}
// Build the DOM elements.
var outerDiv = document.createElement('nav');
outerDiv.setAttribute('aria-labelledby', id + '-label');
// Vector getting vector-menu-empty FIXME TODO
outerDiv.className = outerDivClass + ' emptyPortlet';
outerDiv.id = id;
if (nextnode && nextnode.parentNode === root) {
root.insertBefore(outerDiv, nextnode);
} else {
root.appendChild(outerDiv);
}
var h3 = document.createElement('h3');
h3.id = id + '-label';
var ul = document.createElement('ul');
if (mw.config.get( "skin" ) === 'vector' || mw.config.get("skin") === 'vector-2022') {
h3.className = "vector-menu-heading";
// add invisible checkbox to keep menu open when clicked
// similar to the p-cactions ("More") menu
if (outerDivClass.indexOf('vector-menu-dropdown') !== -1) {
var chkbox = document.createElement('input');
chkbox.className = 'vectorMenuCheckbox vector-menu-checkbox'; // remove vectorMenuCheckbox after 1.35-wmf.37 goes live
chkbox.setAttribute('type', 'checkbox');
chkbox.setAttribute('aria-labelledby', id + '-label');
outerDiv.appendChild(chkbox);
var span = document.createElement('span');
span.appendChild(document.createTextNode(text));
h3.appendChild(span);
var a = document.createElement('a');
a.href = '#';
$(a).click(function(e) {
e.preventDefault();
});
h3.appendChild(a);
}
outerDiv.appendChild(h3);
ul.className = 'menu vector-menu-content-list'; // remove menu after 1.35-wmf.37 goes live
} else {
h3.appendChild(document.createTextNode(text));
outerDiv.appendChild(h3);
}
if (innerDivClass) {
var innerDiv = document.createElement('div');
innerDiv.className = innerDivClass;
innerDiv.appendChild(ul);
outerDiv.appendChild(innerDiv);
} else {
outerDiv.appendChild(ul);
}
return outerDiv;
}
/**
* **************** twAddPortletLink() ****************
* Builds a portlet menu if it doesn't exist yet, and add the portlet link.
* @param task: Either a URL for the portlet link or a function to execute.
*/
function twAddPortletLink( task, text, id, tooltip )
{
if ( Twinkle.getPref("portletArea") !== null ) {
twAddPortlet( Twinkle.getPref( "portletArea" ), Twinkle.getPref( "portletId" ), Twinkle.getPref( "portletName" ), Twinkle.getPref( "portletType" ), Twinkle.getPref( "portletNext" ));
}
var link = mw.util.addPortletLink( Twinkle.getPref( "portletId" ), typeof task === "string" ? task : "#", text, id, tooltip );
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
if ( $.isFunction( task ) ) {
$( link ).click(function ( ev ) {
task();
ev.preventDefault();
});
}
if ($.collapsibleTabs) {
$.collapsibleTabs.handleResize();
}
return link;
}
// Check if account is experienced enough to use Twinkle
var twinkleUserAuthorized = Morebits.userIsInGroup( "autoconfirmed" ) || Morebits.userIsInGroup( "confirmed" ) || Morebits.userIsInGroup( "sysop" );
/*
****************************************
*** friendlyshared.js: Shared IP tagging module
****************************************
* Mode of invocation: Tab ("Shared")
* Active on: Existing IP user talk pages
* Config directives in: FriendlyConfig
*/
Twinkle.shared = function friendlyshared() {
if( mw.config.get('wgNamespaceNumber') === 3 && Morebits.isIPAddress(mw.config.get('wgTitle')) ) {
var username = mw.config.get('wgTitle').split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes
twAddPortletLink( function(){ Twinkle.shared.callback(username); }, "Shared IP", "friendly-shared", "Shared IP tagging" );
}
};
Twinkle.shared.callback = function friendlysharedCallback( uid ) {
var Window = new Morebits.simpleWindow( 600, 400 );
Window.setTitle( "Shared IP address tagging" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#shared" );
var form = new Morebits.quickForm( Twinkle.shared.callback.evaluate );
var div = form.append( { type: 'div', id: 'sharedip-templatelist' } );
div.append( { type: 'header', label: 'Shared IP address templates' } );
div.append( { type: 'radio', name: 'shared', list: Twinkle.shared.standardList,
event: function( e ) {
Twinkle.shared.callback.change_shared( e );
e.stopPropagation();
}
} );
var org = form.append( { type:'field', label:'Fill in other details (optional) and click \"Submit\"' } );
org.append( {
type: 'input',
name: 'organization',
label: 'IP address owner/operator',
disabled: true,
tooltip: 'You can optionally enter the name of the organization that owns/operates the IP address. You can use wikimarkup if necessary.'
}
);
org.append( {
type: 'input',
name: 'host',
label: 'Host name (optional)',
disabled: true,
tooltip: 'The host name (for example, proxy.example.com) can be optionally entered here and will be linked by the template.'
}
);
org.append( {
type: 'input',
name: 'contact',
label: 'Contact information (only if requested)',
disabled: true,
tooltip: 'You can optionally enter some contact details for the organization. Use this parameter only if the organization has specifically requested that it be added. You can use wikimarkup if necessary.'
}
);
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
$(result).find('div#sharedip-templatelist').addClass('quickform-scrollbox');
};
Twinkle.shared.standardList = [
{
label: '{{SharedIP}}: standard shared IP address template',
value: 'Shared IP',
tooltip: 'IP user talk page template that shows helpful information to IP users and those wishing to warn, block or ban them'
},
{
label: '{{SchoolIP}}: shared IP address template modified for educational institutions',
value: 'SchoolIP'
},
{
label: '{{SharedIPCORP}}: shared IP address template modified for businesses',
value: 'SharedIPCORP'
},
{
label: '{{ISP}}: shared IP address template modified for ISP organizations (specifically proxies)',
value: 'ISP'
}
];
Twinkle.shared.callback.change_shared = function friendlysharedCallbackChangeShared(e) {
if( e.target.value === 'Shared IP edu' ) {
e.target.form.contact.disabled = false;
} else {
e.target.form.contact.disabled = true;
}
e.target.form.organization.disabled=false;
e.target.form.host.disabled=false;
};
Twinkle.shared.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters();
var pageText = pageobj.getPageText();
var found = false;
var text = '{{';
for( var i=0; i < Twinkle.shared.standardList.length; i++ ) {
var tagRe = new RegExp( '(\\{\\{' + Twinkle.shared.standardList[i].value + '(\\||\\}\\}))', 'im' );
if( tagRe.exec( pageText ) ) {
Morebits.status.warn( 'Info', 'Found {{' + Twinkle.shared.standardList[i].value + '}} on the user\'s talk page already...aborting' );
found = true;
}
}
if( found ) {
return;
}
Morebits.status.info( 'Info', 'Will add the shared IP address template to the top of the user\'s talk page.' );
text += params.value + '|' + params.organization;
if( params.value === 'shared IP edu' && params.contact !== '') {
text += '|' + params.contact;
}
if( params.host !== '' ) {
text += '|host=' + params.host;
}
text += '}}\n\n';
var summaryText = 'Added {{[[Template:' + params.value + '|' + params.value + ']]}} template.';
pageobj.setPageText(text + pageText);
pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markSharedIPAsMinor'));
pageobj.setCreateOption('recreate');
pageobj.save();
}
};
Twinkle.shared.callback.evaluate = function friendlysharedCallbackEvaluate(e) {
var shared = e.target.getChecked( 'shared' );
if( !shared || shared.length <= 0 ) {
alert( 'You must select a shared IP address template to use!' );
return;
}
var value = shared[0];
if( e.target.organization.value === '') {
alert( 'You must input an organization for the {{' + value + '}} template!' );
return;
}
var params = {
value: value,
organization: e.target.organization.value,
host: e.target.host.value,
contact: e.target.contact.value
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.shared.callbacks.main);
};
/*
****************************************
*** friendlytag.js: Tag module
****************************************
* Mode of invocation: Tab ("Tag")
* Active on: Existing articles; file pages with a corresponding file
* which is local (not on Commons); existing subpages of
* {Wikipedia|Wikipedia talk}:Articles for creation;
* all redirects
* Config directives in: FriendlyConfig
*/
Twinkle.tag = function friendlytag() {
// redirect tagging
if( Morebits.wiki.isPageRedirect() ) {
Twinkle.tag.mode = 'redirect';
//twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Tag redirect" );
}
// file tagging
else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) {
Twinkle.tag.mode = 'file';
}
// article/draft article tagging
else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) {
Twinkle.tag.mode = 'article';
//twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Add maintenance tags to article" );
}
};
Twinkle.tag.callback = function friendlytagCallback( uid ) {
var Window = new Morebits.simpleWindow( 630, (Twinkle.tag.mode === "article") ? 450 : 400 );
Window.setScriptName( "Twinkle" );
// anyone got a good policy/guideline/info page/instructional page link??
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#tag" );
var form = new Morebits.quickForm( Twinkle.tag.callback.evaluate );
switch( Twinkle.tag.mode ) {
case 'article':
Window.setTitle( "Article maintenance tagging" );
form.append( {
type: 'checkbox',
list: [
{
label: 'Group inside {{multiple issues}} if possible',
value: 'group',
name: 'group',
tooltip: 'If applying three or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.',
checked: Twinkle.getFriendlyPref('groupByDefault')
}
]
}
);
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (mh:dev:Twinkle/Preferences).',
event: Twinkle.tag.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'alpha' }
]
});
form.append( { type: 'div', id: 'tagWorkArea' } );
if( Twinkle.getFriendlyPref('customTagList').length ) {
form.append( { type: 'header', label: 'Custom tags' } );
form.append( { type: 'checkbox', name: 'articleTags', list: Twinkle.getFriendlyPref('customTagList') } );
}
break;
case 'redirect':
Window.setTitle( "Redirect tagging" );
//Spelling, misspelling, tense and capitalization templates
form.append({ type: 'header', label:'All templates' });
form.append({ type: 'checkbox', name: 'redirectTags', list: Twinkle.tag.spellingList });
break;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
if (Twinkle.tag.mode === "article") {
// fake a change event on the sort dropdown, to initialize the tag list
var evt = document.createEvent("Event");
evt.initEvent("change", true, true);
result.sortorder.dispatchEvent(evt);
}
};
Twinkle.tag.checkedTags = [];
Twinkle.tag.updateSortOrder = function(e) {
var sortorder = e.target.value;
var $workarea = $(e.target.form).find("div#tagWorkArea");
Twinkle.tag.checkedTags = e.target.form.getChecked("articleTags");
if (!Twinkle.tag.checkedTags) {
Twinkle.tag.checkedTags = [];
}
// function to generate a checkbox, with appropriate subgroup if needed
var makeCheckbox = function(tag, description) {
var checkbox = { value: tag, label: "{{" + tag + "}}: " + description };
if (Twinkle.tag.checkedTags.indexOf(tag) !== -1) {
checkbox.checked = true;
}
if (tag === "notability") {
checkbox.subgroup = {
name: 'notability',
type: 'select',
list: [
{ label: "{{notability}}: article\'s subject may not meet the general notability guideline", value: "none" },
{ label: "{{notability|Academics}}: notability guideline for academics", value: "Academics" },
{ label: "{{notability|Biographies}}: notability guideline for biographies", value: "Biographies" },
{ label: "{{notability|Books}}: notability guideline for books", value: "Books" },
{ label: "{{notability|Companies}}: notability guidelines for companies and organizations", value: "Companies" },
{ label: "{{notability|Events}}: notability guideline for events", value: "Events" },
{ label: "{{notability|Films}}: notability guideline for films", value: "Films" },
{ label: "{{notability|Music}}: notability guideline for music", value: "Music" },
{ label: "{{notability|Neologisms}}: notability guideline for neologisms", value: "Neologisms" },
{ label: "{{notability|Numbers}}: notability guideline for numbers", value: "Numbers" },
{ label: "{{notability|Products}}: notability guideline for products and services", value: "Products" },
{ label: "{{notability|Sport}}: notability guideline for sports and athletics", value: "Sport" },
{ label: "{{notability|Web}}: notability guideline for web content", value: "Web" }
]
};
}
return checkbox;
};
// categorical sort order
if (sortorder === "cat") {
var div = new Morebits.quickForm.element({
type: "div",
id: "tagWorkArea"
});
// function to iterate through the tags and create a checkbox for each one
var doCategoryCheckboxes = function(subdiv, array) {
var checkboxes = [];
$.each(array, function(k, tag) {
var description = Twinkle.tag.article.tags[tag];
checkboxes.push(makeCheckbox(tag, description));
});
subdiv.append({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
};
var i = 0;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.tag.article.tagCategories, function(title, content) {
div.append({ type: "header", id: "tagHeader" + i, label: title });
var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ });
if ($.isArray(content)) {
doCategoryCheckboxes(subdiv, content);
} else {
$.each(content, function(subtitle, subcontent) {
subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] });
doCategoryCheckboxes(subdiv, subcontent);
});
}
});
var rendered = div.render();
$workarea.replaceWith(rendered);
var $rendered = $(rendered);
$rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' });
$rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' });
}
// alphabetical sort order
else {
var checkboxes = [];
$.each(Twinkle.tag.article.tags, function(tag, description) {
checkboxes.push(makeCheckbox(tag, description));
});
var tags = new Morebits.quickForm.element({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
$workarea.empty().append(tags.render());
}
};
// Tags for ARTICLES start here
Twinkle.tag.article = {};
// A list of all article tags, in alphabetical order
// To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below.
Twinkle.tag.article.tags = {
"advertisement": "article is written like an advertisement",
"autobiography": "article is an autobiography and may not be written neutrally",
"BLP sources": "BLP article needs more sources for verification",
"BLP unsourced": "BLP article has no sources at all",
"citation style": "article has unclear or inconsistent inline citations",
"cleanup": "article may require cleanup",
"COI": "article creator or major contributor may have a conflict of interest",
"complex": "the English used in this article or section may not be easy for everybody to understand",
"confusing": "article may be confusing or unclear",
"context": "article provides insufficient context",
"copyedit": "article needs copy editing for grammar, style, cohesion, tone, and/or spelling",
"dead end": "article has few or no links to other articles",
"disputed": "article has questionable factual accuracy",
"expert-subject": "article needs attention from an expert on the subject",
"external links": "article's external links may not follow content policies or guidelines",
"fansite": "article resembles a fansite",
"fiction": "article fails to distinguish between fact and fiction",
"globalise": "article may not represent a worldwide view of the subject",
"hoax": "article may be a complete hoax",
"in-universe": "article subject is fictional and needs rewriting from a non-fictional perspective",
"in use": "article is undergoing a major edit for a short while",
"intro-missing": "article has no lead section and one should be written",
"intro-rewrite": "article lead section needs to be rewritten",
"intro-tooshort": "article lead section is too short and should be expanded",
"jargon": "article uses technical words that not everybody will know",
"link rot": "article uses bare URLs for references, which are prone to link rot",
"merge": "article should be merged with another given article",
"metricate": "article exclusively uses non-SI units of measurement",
"more footnotes": "article has some references, but insufficient in-text citations",
"more sources": "article needs more sources for verification",
"no footnotes": "article has references, but no in-text citations",
"no sources": "article has no references at all",
"notability": "article's subject may not meet the notability guideline",
"NPOV": "article does not maintain a neutral point of view",
"one source": "article relies largely or entirely upon a single source",
"original research": "article has original research or unverified claims",
"orphan": "article is linked to from no other articles",
"plot": "plot summary in article is too long",
"primary sources": "article relies too heavily on first-hand sources, and needs third-party sources",
"prose": "article is in a list format that may be better presented using prose",
"redlinks": "article may have too many red links",
"restructure": "article may be in need of reorganization to comply with Wikipedia's layout guidelines",
"rough translation": "article is poorly translated and needs cleanup",
"sections": "article needs to be broken into sections",
"self-published": "article may contain improper references to self-published sources",
"tone": "tone of article is not appropriate",
"uncat": "article is uncategorized",
"under construction": "article is currently in the middle of an expansion or major revamping",
"unreliable sources": "article's references may not be reliable sources",
"update": "article needs additional up-to-date information added",
"very long": "article is too long",
"weasel": "article neutrality is compromised by the use of weasel words",
"wikify": "article needs to be wikified"
};
// A list of tags in order of category
// Tags should be in alphabetical order within the categories
// Add new categories with discretion - the list is long enough as is!
Twinkle.tag.article.tagCategories = {
"Cleanup and maintenance tags": {
"General maintenance tags": [
"cleanup",
"complex",
"copyedit",
"wikify"
],
"Potentially unwanted content": [
"external links"
],
"Structure, formatting, and lead section": [
"intro-missing",
"intro-rewrite",
"intro-tooshort",
"restructure",
"sections",
"very long"
],
"Fiction-related cleanup": [
"fiction",
"in-universe",
"plot"
]
},
"General content issues": {
"Importance and notability": [
"notability" // has subcategories and special-cased code
],
"Style of writing": [
"advertisement",
"fansite",
"jargon",
"prose",
"redlinks",
"tone"
],
"Sense (or lack thereof)": [
"confusing"
],
"Information and detail": [
"context",
"expert-subject",
"metricate"
],
"Timeliness": [
"update"
],
"Neutrality, bias, and factual accuracy": [
"autobiography",
"COI",
"disputed",
"hoax",
"globalise",
"NPOV",
"weasel"
],
"Verifiability and sources": [
"BLP sources",
"BLP unsourced",
"more sources",
"no sources",
"one source",
"original research",
"primary sources",
"self-published",
"unreliable sources"
]
},
"Specific content issues": {
"Language": [
"complex"
],
"Links": [
"dead end",
"orphan",
"wikify" // this tag is listed twice because it used to focus mainly on links, but now it's a more general cleanup tag
],
"Referencing technique": [
"citation style",
"link rot",
"more footnotes",
"no footnotes"
],
"Categories": [
"uncat"
]
},
"Merging": [
"merge",
],
"Informational": [
"in use",
"under construction"
]
};
// Tags for REDIRECTS start here
Twinkle.tag.spellingList = [
{
label: '{{R from capitalization}}: redirect from a from a capitalized title',
value: 'R from capitalization'
},
{
label: '{{R with other capitalizations}}: redirect from a title with a different capitalization',
value: 'R with other capitalizations'
},
{
label: '{{R from other name}}: redirect from a title with a different name',
value: 'R from other name'
},
{
label: '{{R from other spelling}}: redirect from a title with a different spelling',
value: 'R from other spelling'
},
{
label: '{{R from plural}}: redirect from a plural title',
value: 'R from plural'
},
{
label: '{{R from related things}}: redirect related title',
value: 'R from related things'
},
{
label: '{{R to section}}: redirect from a title for a "minor topic or title" to a comprehensive-type article section which covers the subject',
value: 'R to section'
},
{
label: '{{R from shortcut}}: redirect to a Wikipedia "shortcut"',
value: 'R from shortcut'
},
{
label: '{{R from title without diacritics}}: redirect to the article title with diacritical marks (accents, umlauts, etc.)',
value: 'R from title without diacritics'
}
];
// Contains those article tags that *do not* work inside {{multiple issues}}.
Twinkle.tag.multipleIssuesExceptions = [
'cat improve',
'in use',
'merge',
'merge from',
'merge to',
'not English',
'rough translation',
'uncat',
'under construction',
];
Twinkle.tag.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters(),
tagRe, tagText = '', summaryText = 'Added',
tags = [], groupableTags = [], i, totalTags
var pageText = pageobj.getPageText();
var addTag = function friendlytagAddTag( tagIndex, tagName ) {
var currentTag = "";
if( tagName === 'globalize' ) {
currentTag += '{{' + params.globalizeSubcategory;
} else {
currentTag += ( Twinkle.tag.mode === 'redirect' ? '\n' : '' ) + '{{' + tagName;
}
if( tagName === 'notability' && params.notabilitySubcategory !== 'none' ) {
currentTag += '|' + params.notabilitySubcategory;
}
// prompt for other parameters, based on the tag
switch( tagName ) {
case 'cleanup':
var reason = prompt('"The specific problem is: " \n' +
"This information is optional. Just click OK if you don't wish to enter this.", "");
if (reason === null) {
Morebits.status.warn("Notice", "{{cleanup}} tag skipped by user");
return true; // continue to next tag
} else {
currentTag += '|reason=' + reason;
}
break;
case 'complex':
var cpreason = prompt('"An editor’s reason for this is:" (e.g. "words like XX") \n' +
"Just click OK if you don't wish to enter this. To skip the {{complex}} tag, click Cancel.", "");
if (cpreason === null) {
return true; // continue to next tag
} else if (cpreason !== "") {
currentTag += '|2=' + cpreason;
}
break;
case 'copyedit':
var cereason = prompt('"This article may require copy editing for..." (e.g. "consistent spelling") \n' +
"Just click OK if you don't wish to enter this. To skip the {{copyedit}} tag, click Cancel.", "");
if (cereason === null) {
return true; // continue to next tag
} else if (cereason !== "") {
currentTag += '|for=' + cereason;
}
break;
case 'expert-subject':
var esreason = prompt('"This is because..." \n' +
"This information is optional. To skip the {{expert-subject}} tag, click Cancel.", "");
if (esreason === null) {
return true; // continue to next tag
} else if (esreason !== "") {
currentTag += '|1=' + esreason;
}
break;
case 'not English':
var langname = prompt('Please enter the name of the language the article is thought to be written in. \n' +
"Just click OK if you don't know. To skip the {{not English}} tag, click Cancel.", "");
if (langname === null) {
return true; // continue to next tag
} else if (langname !== "") {
currentTag += '|1=' + langname;
}
break;
case 'rough translation':
var roughlang = prompt('Please enter the name of the language the article is thought to have been translated from. \n' +
"Just click OK if you don't know. To skip the {{rough translation}} tag, click Cancel.", "");
if (roughlang === null) {
return true; // continue to next tag
} else if (roughlang !== "") {
currentTag += '|1=' + roughlang;
}
break;
case 'wikify':
var wreason = prompt('You can optionally enter a more specific reason why the article needs to be wikified: This article needs to be wikified. {{{Your reason here}}} \n' +
"Just click OK if you don't wish to enter this. To skip the {{wikify}} tag, click Cancel.", "");
if (wreason === null) {
return true; // continue to next tag
} else if (wreason !== "") {
currentTag += '|reason=' + wreason;
}
break;
case 'merge':
case 'merge to':
case 'merge from':
var param = prompt('Please enter the name of the other article(s) involved in the merge. \n' +
"To specify multiple articles, separate them with a vertical pipe (|) character. \n" +
"This information is required. Click OK when done, or click Cancel to skip the merge tag.", "");
if (param === null) {
return true; // continue to next tag
} else if (param !== "") {
currentTag += '|' + param;
}
break;
default:
break;
}
currentTag += (Twinkle.tag.mode === 'redirect') ? '}}' : '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n';
tagText += currentTag;
if ( tagIndex > 0 ) {
if( tagIndex === (totalTags - 1) ) {
summaryText += ' and';
} else if ( tagIndex < (totalTags - 1) ) {
summaryText += ',';
}
}
summaryText += ' {{[[';
summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName));
summaryText += ']]}}';
};
if( Twinkle.tag.mode !== 'redirect' ) {
// Check for preexisting tags and separate tags into groupable and non-groupable arrays
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
if( Twinkle.tag.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) {
groupableTags = groupableTags.concat( params.tags[i] );
} else {
tags = tags.concat( params.tags[i] );
}
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the article already...excluding' );
}
}
if( params.group && groupableTags.length >= 3 ) {
Morebits.status.info( 'Info', 'Grouping supported tags inside {{multiple issues}}' );
groupableTags.sort();
tagText += '{{multiple issues|\n';
totalTags = groupableTags.length;
$.each(groupableTags, addTag);
summaryText += ' tags (within {{[[Template:multiple issues|multiple issues]]}})';
if( tags.length > 0 ) {
summaryText += ', and';
}
tagText += '}}\n';
} else {
tags = tags.concat( groupableTags );
}
} else {
// Redirect tagging: Check for pre-existing tags
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
tags = tags.concat( params.tags[i] );
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the redirect already...excluding' );
}
}
}
tags.sort();
totalTags = tags.length;
$.each(tags, addTag);
if( Twinkle.tag.mode === 'redirect' ) {
pageText += tagText;
} else {
// smartly insert the new tags after any hatnotes. Regex is a bit more
// complicated than it'd need to be, to allow templates as parameters,
// and to handle whitespace properly.
pageText = pageText.replace(/^\s*(?:((?:\s*\{\{\s*(?:about|correct title|dablink|distinguish|for|other\s?(?:hurricaneuses|people|persons|places|uses(?:of)?)|redirect(?:-acronym)?|see\s?(?:also|wiktionary)|selfref|the)\d*\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\})+(?:\s*\n)?)\s*)?/i,
"$1" + tagText);
}
summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) +
' to ' + Twinkle.tag.mode + Twinkle.getPref('summaryAd');
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) {
pageobj.patrol();
}
},
file: function friendlytagCallbacksFile(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var summary = "Adding ";
// Add maintenance tags
if (params.tags.length) {
var tagtext = "", currentTag;
$.each(params.tags, function(k, tag) {
currentTag += "}}\n";
tagtext += currentTag;
summary += "{{" + tag + "}}, ";
return true; // continue
});
if (!tagtext) {
pageobj.getStatusElement().warn("User canceled operation; nothing to do");
return;
}
text = tagtext + text;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary.substring(0, summary.length - 2) + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) {
pageobj.patrol();
}
}
};
Twinkle.tag.callback.evaluate = function friendlytagCallbackEvaluate(e) {
var form = e.target;
var params = {};
switch (Twinkle.tag.mode) {
case 'article':
params.tags = form.getChecked( 'articleTags' );
params.group = form.group.checked;
params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null;
break;
case 'file':
params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null;
params.tags = form.getChecked( 'imageTags' );
break;
case 'redirect':
params.tags = form.getChecked( 'redirectTags' );
break;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
if( !params.tags.length ) {
alert( 'You must select at least one tag!' );
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds";
if (Twinkle.tag.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.tag.mode);
wikipedia_page.setCallbackParameters(params);
switch (Twinkle.tag.mode) {
case 'article':
/* falls through */
case 'redirect':
wikipedia_page.load(Twinkle.tag.callbacks.main);
return;
case 'file':
wikipedia_page.load(Twinkle.tag.callbacks.file);
return;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
};
/*
****************************************
*** twinklestub.js: Tag module
****************************************
* Mode of invocation: Tab ("Stub")
* Active on: Existing articles
* Config directives in: FriendlyConfig
* Note: customised friendlytag module (for SEWP)
*/
Twinkle.stub = function friendlytag() {
// redirect tagging
if( Morebits.wiki.isPageRedirect() ) {
Twinkle.stub.mode = 'redirect';
}
// file tagging
else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) {
Twinkle.stub.mode = 'file';
}
// article/draft article tagging
else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) {
Twinkle.stub.mode = 'article';
//twAddPortletLink( Twinkle.stub.callback, "Stub", "friendly-tag", "Add stub tags to article" );
}
};
Twinkle.stub.callback = function friendlytagCallback( uid ) {
var Window = new Morebits.simpleWindow( 630, (Twinkle.stub.mode === "article") ? 450 : 400 );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Simple Stub project", "Wikipedia:Simple Stub Project" );
Window.addFooterLink( "Stub guideline", "Wikipedia:Stub" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#stub" );
var form = new Morebits.quickForm( Twinkle.stub.callback.evaluate );
switch( Twinkle.stub.mode ) {
case 'article':
Window.setTitle( "Article stub tagging" );
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (https://dev.miraheze.org/wiki/Twinkle/Preferences).',
event: Twinkle.stub.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'alpha' }
]
});
form.append( { type: 'div', id: 'tagWorkArea' } );
}
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
if (Twinkle.stub.mode === "article") {
// fake a change event on the sort dropdown, to initialize the tag list
var evt = document.createEvent("Event");
evt.initEvent("change", true, true);
result.sortorder.dispatchEvent(evt);
}
};
Twinkle.stub.checkedTags = [];
Twinkle.stub.updateSortOrder = function(e) {
var sortorder = e.target.value;
var $workarea = $(e.target.form).find("div#tagWorkArea");
Twinkle.stub.checkedTags = e.target.form.getChecked("articleTags");
if (!Twinkle.stub.checkedTags) {
Twinkle.stub.checkedTags = [];
}
// function to generate a checkbox, with appropriate subgroup if needed
var makeCheckbox = function(tag, description) {
var checkbox = { value: tag, label: "{{" + tag + "}}: " + description };
if (Twinkle.stub.checkedTags.indexOf(tag) !== -1) {
checkbox.checked = true;
}
return checkbox;
};
// categorical sort order
if (sortorder === "cat") {
var div = new Morebits.quickForm.element({
type: "div",
id: "tagWorkArea"
});
// function to iterate through the tags and create a checkbox for each one
var doCategoryCheckboxes = function(subdiv, array) {
var checkboxes = [];
$.each(array, function(k, tag) {
var description = Twinkle.stub.article.tags[tag];
checkboxes.push(makeCheckbox(tag, description));
});
subdiv.append({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
};
var i = 0;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.stub.article.tagCategories, function(title, content) {
div.append({ type: "header", id: "tagHeader" + i, label: title });
var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ });
if ($.isArray(content)) {
doCategoryCheckboxes(subdiv, content);
} else {
$.each(content, function(subtitle, subcontent) {
subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] });
doCategoryCheckboxes(subdiv, subcontent);
});
}
});
var rendered = div.render();
$workarea.replaceWith(rendered);
var $rendered = $(rendered);
$rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' });
$rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' });
}
// alphabetical sort order
else {
var checkboxes = [];
$.each(Twinkle.stub.article.tags, function(tag, description) {
checkboxes.push(makeCheckbox(tag, description));
});
var tags = new Morebits.quickForm.element({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
$workarea.empty().append(tags.render());
}
};
// Tags for ARTICLES start here
Twinkle.stub.article = {};
// A list of all article tags, in alphabetical order
// To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below.
Twinkle.stub.article.tags = {
"actor-stub": "for use with articles about actors",
"asia-stub": "for use with anything about Asia, except people",
"bio-stub": "for use with all people, no matter who or what profession",
"biology-stub": "for use with topics related to biology",
"chem-stub": "for use with topics related to chemistry",
"europe-stub": "for use with anything about Europe, except people",
"france-geo-stub": "for use with France geography topics",
"food-stub": "for use with anything about food",
"geo-stub": "for use with all geographical locations (places, towns, cities, etc)",
"history-stub": "for use with history topics",
"japan-stub": "for use with anything about Japan, except people",
"japan-sports-bio-stub": "for use with Japanese sport biographies",
"list-stub": "for use with lists only",
"lit-stub": "for use with all literature articles except people",
"math-stub": "for use with topics related to mathematics",
"med-stub": "for use with topics related to medicine",
"military-stub": "for use with military related topics",
"movie-stub": "for use with all movie articles except people",
"music-stub": "for use with all music articles except people",
"north-America-stub": "for use with anything about North America, except people",
"performing-arts-stub": "general stub for the performing arts",
"physics-stub": "for use with topics related to physics",
"politics-stub": "for use with politics related topics",
"religion-stub": "for use with religion related topics",
"sci-stub": "anything science related (all branches and their tools)",
"sport-stub": "general stub for all sports and sports items, not people",
"sports-bio-stub": "for use with people who have sport as profession",
"stub": "for all stubs that can not fit into any stub we have",
"switzerland-stub": "for use with everything about Switzerland, except people",
"tech-stub": "for use with technology related articles",
"transport-stub": "for use with articles about any moving object (cars, bikes, ships, crafts, planes, rail, buses, trains, etc)",
"tv-stub": "for use with all television articles except people",
"UK-stub": "for use with anything about the United Kingdom, except people",
"US-actor-stub": "for use with United States actor biographies",
"US-bio-stub": "for use with United States biographies",
"US-geo-stub": "for use with United States geography topics",
"US-stub": "for use with anything about the United States, except people and geography",
"video-game-stub": "for use with stubs related to video games",
"weather-stub": "for articles about weather"
};
// A list of tags in order of category
// Tags should be in alphabetical order within the categories
// Add new categories with discretion - the list is long enough as is!
Twinkle.stub.article.tagCategories = {
"Stub templates": [
"stub",
"list-stub"
],
"Countries & Geography": [
"asia-stub",
"europe-stub",
"france-geo-stub",
"geo-stub",
"japan-stub",
"japan-sports-bio-stub",
"north-America-stub",
"switzerland-stub",
"UK-stub",
"US-bio-stub",
"US-geo-stub",
"US-stub"
],
"Miscellaneous": [
"food-stub",
"history-stub",
"military-stub",
"politics-stub",
"religion-stub",
"transport-stub"
],
"People": [
"actor-stub",
"bio-stub",
"japan-sports-bio-stub",
"sports-bio-stub",
"US-actor-stub",
"US-bio-stub"
],
"Science": [
"biology-stub",
"chem-stub",
"math-stub",
"med-stub",
"physics-stub",
"sci-stub",
"weather-stub"
],
"Sports": [
"japan-sports-bio-stub",
"sport-stub",
"sports-bio-stub"
],
"Technology": [
"tech-stub",
"video-game-stub"
],
"Arts": [
"actor-stub",
"lit-stub",
"movie-stub",
"music-stub",
"performing-arts-stub",
"tv-stub",
"US-actor-stub"
]
}
// Tags for REDIRECTS start here
// Contains those article tags that *do not* work inside {{multiple issues}}.
Twinkle.stub.multipleIssuesExceptions = [
'cat improve',
'in use',
'merge',
'merge from',
'merge to',
'not English',
'rough translation',
'uncat',
'under construction',
'update'
];
Twinkle.stub.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters(),
tagRe, tagText = '', summaryText = 'Added',
tags = [], groupableTags = [], i, totalTags;
// Remove tags that become superfluous with this action
var pageText = pageobj.getPageText();
var addTag = function friendlytagAddTag( tagIndex, tagName ) {
var currentTag = "";
pageText += '\n\n{{' + tagName + '}}';
if ( tagIndex > 0 ) {
if( tagIndex === (totalTags - 1) ) {
summaryText += ' and';
} else if ( tagIndex < (totalTags - 1) ) {
summaryText += ',';
}
}
summaryText += ' {{[[';
summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName));
summaryText += ']]}}';
};
// Check for preexisting tags and separate tags into groupable and non-groupable arrays
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
if( Twinkle.stub.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) {
groupableTags = groupableTags.concat( params.tags[i] );
} else {
tags = tags.concat( params.tags[i] );
}
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the article already...excluding' );
}
}
tags = tags.concat( groupableTags );
tags.sort();
totalTags = tags.length;
$.each(tags, addTag);
summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) +
' to ' + Twinkle.stub.mode + Twinkle.getPref('summaryAd');
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchStubbedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markStubbedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markStubbedPagesAsPatrolled') ) {
pageobj.patrol();
}
}
};
Twinkle.stub.callback.evaluate = function friendlytagCallbackEvaluate(e) {
var form = e.target;
var params = {};
switch (Twinkle.stub.mode) {
case 'article':
params.tags = form.getChecked( 'articleTags' );
params.group = false;
params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null;
break;
case 'file':
params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null;
params.tags = form.getChecked( 'imageTags' );
break;
case 'redirect':
params.tags = form.getChecked( 'redirectTags' );
break;
}
if( !params.tags.length ) {
alert( 'You must select at least one tag!' );
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds";
if (Twinkle.stub.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.stub.mode);
wikipedia_page.setCallbackParameters(params);
switch (Twinkle.stub.mode) {
case 'article':
/* falls through */
case 'redirect':
wikipedia_page.load(Twinkle.stub.callbacks.main);
return;
case 'file':
wikipedia_page.load(Twinkle.stub.callbacks.file);
return;
}
};
/*
****************************************
*** friendlytalkback.js: Talkback module
****************************************
* Mode of invocation: Tab ("TB")
* Active on: Existing user talk pages
* Config directives in: FriendlyConfig
*/
;(function(){
Twinkle.talkback = function() {
if ( Morebits.getPageAssociatedUser() === false ) {
return;
}
twAddPortletLink( callback, "TB", "friendly-talkback", "Easy talkback" );
};
var callback = function( ) {
if( Morebits.getPageAssociatedUser() === mw.config.get("wgUserName") && !confirm("Is it really so bad that you're talking back to yourself?") ){
return;
}
var Window = new Morebits.simpleWindow( 600, 350 );
Window.setTitle("Talkback");
Window.setScriptName("Twinkle");
Window.addFooterLink( "About {{talkback}}", "Template:Talkback" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#talkback" );
var form = new Morebits.quickForm( callback_evaluate );
form.append({ type: "radio", name: "tbtarget",
list: [
{
label: "Talkback: my talk page",
value: "mytalk",
checked: "true"
},
{
label: "Talkback: other user talk page",
value: "usertalk"
},
{
label: "Talkback: other page",
value: "other"
},
{
label: "Noticeboard notification",
value: "notice"
},
{
label: "\"You've got mail\"",
value: "mail"
},
{
label: "Whisperback",
value: "wb"
}
],
event: callback_change_target
});
form.append({
type: "field",
label: "Work area",
name: "work_area"
});
form.append({ type: "submit" });
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the
var evt = document.createEvent("Event");
evt.initEvent( "change", true, true );
result.tbtarget[0].dispatchEvent( evt );
};
var prev_page = "";
var prev_section = "";
var prev_message = "";
var callback_change_target = function( e ) {
var value = e.target.values;
var root = e.target.form;
var old_area = Morebits.quickForm.getElements(root, "work_area")[0];
if(root.section) {
prev_section = root.section.value;
}
if(root.message) {
prev_message = root.message.value;
}
if(root.page) {
prev_page = root.page.value;
}
var work_area = new Morebits.quickForm.element({
type: "field",
label: "Talkback information",
name: "work_area"
});
switch( value ) {
case "mytalk":
/* falls through */
default:
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on your talk page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "usertalk":
work_area.append({
type:"input",
name:"page",
label:"User",
tooltip:"The username of the user on whose talk page you left a message.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "notice":
var noticeboard = work_area.append({
type: "select",
name: "noticeboard",
label: "Noticeboard:"
});
noticeboard.append({
type: "option",
label: "WP:AN (Administrators' noticeboard)",
value: "an"
});
work_area.append({
type:"input",
name:"section",
label:"Linked thread",
tooltip:"The heading of the relevant thread on the noticeboard page.",
value: prev_section
});
break;
case "other":
work_area.append({
type:"input",
name:"page",
label:"Full page name",
tooltip:"The full page name where you left the message. For example: 'Wikipedia talk:Twinkle'.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "mail":
work_area.append({
type:"input",
name:"section",
label:"Subject of e-mail (optional)",
tooltip:"The subject line of the e-mail you sent."
});
break;
case "wb":
work_area.append({
type:"input",
name:"page",
label:"User",
tooltip:"The username of the user on whose talk page you left a message.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
}
if (value !== "notice") {
work_area.append({ type:"textarea", label:"Additional message (optional):", name:"message", tooltip:"An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one." });
}
work_area = work_area.render();
root.replaceChild( work_area, old_area );
if (root.message) {
root.message.value = prev_message;
}
};
var callback_evaluate = function( e ) {
var tbtarget = e.target.getChecked( "tbtarget" )[0];
var page = null;
var section = e.target.section.value;
var fullUserTalkPageName = mw.config.get("wgFormattedNamespaces")[ mw.config.get("wgNamespaceIds").user_talk ] + ":" + Morebits.getPageAssociatedUser();
if( tbtarget === "usertalk" || tbtarget === "other" || tbtarget === "wb" ) {
page = e.target.page.value;
if( tbtarget === "usertalk" ) {
if( !page ) {
alert("You must specify the username of the user whose talk page you left a message on.");
return;
}
} else {
if( !page ) {
alert("You must specify the full page name when your message is not on a user talk page.");
return;
}
}
} else if (tbtarget === "notice") {
page = e.target.noticeboard.value;
}
var message;
if (e.target.message) {
message = e.target.message.value;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName;
Morebits.wiki.actionCompleted.notice = "Talkback complete; reloading talk page in a few seconds";
var talkpage = new Morebits.wiki.page(fullUserTalkPageName, "Adding talkback");
var tbPageName = (tbtarget === "mytalk") ? mw.config.get("wgUserName") : page;
var text;
if ( tbtarget === "notice" ) {
text = "\n\n== " + Twinkle.getFriendlyPref("adminNoticeHeading") + " ==\n";
text += "{{subst:AN-notice|thread=" + section + "|noticeboard=Wikipedia:Administrators' noticeboard}} ~~~~";
talkpage.setEditSummary( "Notice of discussion at [[Wikipedia:Administrators' noticeboard]]" + Twinkle.getPref("summaryAd") );
} else if ( tbtarget === "mail" ) {
text = "\n\n==" + Twinkle.getFriendlyPref("mailHeading") + "==\n{{you've got mail|subject=";
text += section + "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Notification: You've got mail" + Twinkle.getPref("summaryAd"));
} else if ( tbtarget === "wb" ) {
text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{wb|";
text += tbPageName;
if( section ) {
text += "|" + section;
}
text += "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Whisperback" + Twinkle.getPref("summaryAd"));
} else {
//clean talkback heading: strip section header markers, were erroneously suggested in the documentation
text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{tb|";
text += tbPageName;
if( section ) {
text += "|" + section;
}
text += "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Talkback ([[" + (tbtarget === "other" ? "" : "User talk:") + tbPageName +
(section ? ("#" + section) : "") + "]])" + Twinkle.getPref("summaryAd"));
}
talkpage.setAppendText( text );
talkpage.setCreateOption("recreate");
talkpage.setMinorEdit(Twinkle.getFriendlyPref("markTalkbackAsMinor"));
talkpage.setFollowRedirect( true );
talkpage.append();
};
}());
/*
****************************************
*** friendlywelcome.js: Welcome module
****************************************
* Mode of invocation: Tab ("Wel"), or from links on diff pages
* Active on: Existing user talk pages, diff pages
* Config directives in: FriendlyConfig
*/
Twinkle.welcome = function friendlywelcome() {
if( Morebits.queryString.exists( 'friendlywelcome' ) ) {
if( Morebits.queryString.get( 'friendlywelcome' ) === 'auto' ) {
Twinkle.welcome.auto();
} else {
Twinkle.welcome.semiauto();
}
} else {
Twinkle.welcome.normal();
}
};
Twinkle.welcome.auto = function() {
if( Morebits.queryString.get( 'action' ) !== 'edit' ) {
// userpage not empty, aborting auto-welcome
return;
}
Twinkle.welcome.welcomeUser();
};
Twinkle.welcome.semiauto = function() {
Twinkle.welcome.callback( mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\"") );
};
Twinkle.welcome.normal = function() {
if( Morebits.queryString.exists( 'diff' ) ) {
// check whether the contributors' talk pages exist yet
var $oList = $("#mw-diff-otitle2").find("span.mw-usertoollinks a.new:contains(talk)").first();
var $nList = $("#mw-diff-ntitle2").find("span.mw-usertoollinks a.new:contains(talk)").first();
if( $oList.length > 0 || $nList.length > 0 ) {
var spanTag = function( color, content ) {
var span = document.createElement( 'span' );
span.style.color = color;
span.appendChild( document.createTextNode( content ) );
return span;
};
var welcomeNode = document.createElement('strong');
var welcomeLink = document.createElement('a');
welcomeLink.appendChild( spanTag( 'Black', '[' ) );
welcomeLink.appendChild( spanTag( 'Goldenrod', 'welcome' ) );
welcomeLink.appendChild( spanTag( 'Black', ']' ) );
welcomeNode.appendChild(welcomeLink);
if( $oList.length > 0 ) {
var oHref = $oList.attr("href");
var oWelcomeNode = welcomeNode.cloneNode( true );
oWelcomeNode.firstChild.setAttribute( 'href', oHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) );
$oList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) );
$oList[0].parentNode.parentNode.appendChild( oWelcomeNode );
}
if( $nList.length > 0 ) {
var nHref = $nList.attr("href");
var nWelcomeNode = welcomeNode.cloneNode( true );
nWelcomeNode.firstChild.setAttribute( 'href', nHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) );
$nList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) );
$nList[0].parentNode.parentNode.appendChild( nWelcomeNode );
}
}
}
if( mw.config.get( 'wgNamespaceNumber' ) === 3 ) {
var username = mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes
twAddPortletLink( function(){ Twinkle.welcome.callback(username); }, "Wel", "friendly-welcome", "Welcome user" );
}
};
Twinkle.welcome.welcomeUser = function welcomeUser() {
Morebits.status.init( document.getElementById('bodyContent') );
var params = {
value: Twinkle.getFriendlyPref('quickWelcomeTemplate'),
article: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '',
mode: 'auto'
};
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.welcome.callbacks.main);
};
Twinkle.welcome.callback = function friendlywelcomeCallback( uid ) {
if( uid === mw.config.get('wgUserName') && !confirm( 'Are you really sure you want to welcome yourself?...' ) ){
return;
}
var Window = new Morebits.simpleWindow( 600, 420 );
Window.setTitle( "Welcome user" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#welcome" );
var form = new Morebits.quickForm( Twinkle.welcome.callback.evaluate );
form.append({
type: 'select',
name: 'type',
label: 'Type of welcome: ',
event: Twinkle.welcome.populateWelcomeList,
list: [
{ type: 'option', value: 'standard', label: 'Standard welcomes', selected: !Morebits.isIPAddress(mw.config.get('wgTitle')) },
{ type: 'option', value: 'anonymous', label: 'Problem user welcomes', selected: Morebits.isIPAddress(mw.config.get('wgTitle')) }
]
});
form.append( { type: 'div', id: 'welcomeWorkArea' } );
form.append( {
type: 'input',
name: 'article',
label: '* Linked article (if supported by template):',
value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ),
tooltip: 'An article might be linked from within the welcome if the template supports it. Leave empty for no article to be linked. Templates that support a linked article are marked with an asterisk.'
} );
var previewlink = document.createElement( 'a' );
$(previewlink).click(function(){
Twinkle.welcome.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = "pointer";
previewlink.textContent = 'Preview';
form.append( { type: 'div', name: 'welcomepreview', label: [ previewlink ] } );
form.append( { type: 'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
// initialize the welcome list
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.type.dispatchEvent( evt );
};
Twinkle.welcome.populateWelcomeList = function(e) {
var type = e.target.value;
var $workarea = $(e.target.form).find("div#welcomeWorkArea");
var div = new Morebits.quickForm.element({
type: "div",
id: "welcomeWorkArea"
});
if ((type === "standard" || type === "anonymous") && Twinkle.getFriendlyPref("customWelcomeList").length) {
div.append({ type: 'header', label: 'Custom welcome templates' });
div.append({
type: 'radio',
name: 'template',
list: Twinkle.getFriendlyPref("customWelcomeList"),
event: Twinkle.welcome.selectTemplate
});
}
var appendTemplates = function(list) {
div.append({
type: 'radio',
name: 'template',
list: list.map(function(obj) {
var properties = Twinkle.welcome.templates[obj];
var result = (properties ? {
value: obj,
label: "{{" + obj + "}}: " + properties.description + (properties.linkedArticle ? "\u00A0*" : ""), // U+00A0 NO-BREAK SPACE
tooltip: properties.tooltip // may be undefined
} : {
value: obj,
label: "{{" + obj + "}}"
});
return result;
}),
event: Twinkle.welcome.selectTemplate
});
};
switch (type) {
case "standard":
div.append({ type: 'header', label: 'General welcome templates' });
appendTemplates([
"welcome",
"welcome2",
"welcome-anon",
"welcome-anon2",
"welcome-en",
"welcome-iw",
"welcomeg",
"welcomeq",
"welcome-personal",
"welcome-school"
]);
break;
case "anonymous":
div.append({ type: 'header', label: 'Problem user welcome templates' });
appendTemplates([
"firstarticle",
"welcomespam",
"welcomenpov",
"welcomevandal"
]);
break;
default:
div.append({ type: 'div', label: 'Twinkle.welcome.populateWelcomeList: something went wrong' });
break;
}
var rendered = div.render();
rendered.className = "quickform-scrollbox";
$workarea.replaceWith(rendered);
var firstRadio = e.target.form.template[0];
firstRadio.checked = true;
Twinkle.welcome.selectTemplate({ target: firstRadio });
};
Twinkle.welcome.selectTemplate = function(e) {
var properties = Twinkle.welcome.templates[e.target.values];
e.target.form.article.disabled = (properties ? !properties.linkedArticle : false);
};
// A list of welcome templates and their properties and syntax
// The four fields that are available are "description", "linkedArticle", "syntax", and "tooltip".
// The three magic words that can be used in the "syntax" field are:
// - $USERNAME$ - replaced by the welcomer's username, depending on user's preferences
// - $ARTICLE$ - replaced by an article name, if "linkedArticle" is true
// - $HEADER$ - adds a level 2 header (most templates already include this)
Twinkle.welcome.templates = {
"welcome": {
description: "standard plain text welcome",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome2": {
description: "welcome with graphic and orange color sheme",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome2|~~~~|art=$ARTICLE$}}"
},
"welcome-anon": {
description: "welcome anonymous user and suggest getting a username",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome-anon|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-anon2": {
description: "like welcome-anon, but with table and colors",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome-anon2|$USERNAME$|art=$ARTICLE$}}"
},
"welcome-en": {
description: "welcome for users from main English Wikipedia",
linkedArticle: false,
syntax: "$HEADER$ {{subst:welcome-en}} ~~~~"
},
"welcome-iw": {
description: "welcome users from another Wikipedia",
linkedArticle: false,
syntax: "$HEADER$ {{subst:welcome-iw}} ~~~~"
},
"welcomeg": {
description: "welcome with blue background",
linkedArticle: true,
syntax: "{{subst:welcomeg|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcomeq": {
description: "like welcomeg but a bit shorter",
linkedArticle: true,
syntax: "{{subst:welcomeq|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-personal": {
description: "a more personal welcome with a plate of cookies",
linkedArticle: true,
syntax: "{{subst:welcome-personal|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-school": {
description: "for welcoming students participating in a class project",
linkedArticle: false,
syntax: "{{subst:welcome-school}} ~~~~"
},
// second group
"firstarticle": {
description: "welcome with note that created page may get deleted",
linkedArticle: true,
syntax: "$HEADER$ {{subst:firstarticle|1=$ARTICLE$}} ~~~~"
},
"welcomespam": {
description: "welcome users which did spam changes",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomespam|art=$ARTICLE$}} ~~~~"
},
"welcomenpov": {
description: "welcome with warning to make changes that fit the NPOV requirements",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomenpov|$ARTICLE$}} ~~~~"
},
"welcomevandal": {
description: "welcome user which performed vandalism",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomevandal|$ARTICLE$}} ~~~~"
}
};
Twinkle.welcome.getTemplateWikitext = function(template, article) {
var properties = Twinkle.welcome.templates[template];
if (properties) {
return properties.syntax.
replace("$USERNAME$", Twinkle.getFriendlyPref("insertUsername") ? mw.config.get("wgUserName") : "").
replace("$ARTICLE$", article ? article : "").
replace(/\$HEADER\$\s*/, "== Welcome ==\n\n").
replace("$EXTRA$", ""); // EXTRA is not implemented yet
} else {
return "{{subst:" + template + (article ? ("|art=" + article) : "") + "}} ~~~~";
}
};
Twinkle.welcome.callbacks = {
preview: function(form) {
var previewDialog = new Morebits.simpleWindow(750, 400);
previewDialog.setTitle("Welcome template preview");
previewDialog.setScriptName("Welcome user");
previewDialog.setModality(true);
var previewdiv = document.createElement("div");
previewdiv.style.marginLeft = previewdiv.style.marginRight = "0.5em";
previewdiv.style.fontSize = "small";
previewDialog.setContent(previewdiv);
var previewer = new Morebits.wiki.preview(previewdiv);
previewer.beginRender(Twinkle.welcome.getTemplateWikitext(form.getChecked("template"), form.article.value));
var submit = document.createElement("input");
submit.setAttribute("type", "submit");
submit.setAttribute("value", "Close");
previewDialog.addContent(submit);
previewDialog.display();
$(submit).click(function(e) {
previewDialog.close();
});
},
main: function( pageobj ) {
var params = pageobj.getCallbackParameters();
var text = pageobj.getPageText();
// abort if mode is auto and form is not empty
if( pageobj.exists() && params.mode === 'auto' ) {
Morebits.status.info( 'Warning', 'User talk page not empty; aborting automatic welcome' );
Morebits.wiki.actionCompleted.event();
return;
}
var welcomeText = Twinkle.welcome.getTemplateWikitext(params.value, params.article);
if( Twinkle.getFriendlyPref('topWelcomes') ) {
text = welcomeText + '\n\n' + text;
} else {
text += "\n" + welcomeText;
}
var summaryText = "Welcome to Wikipedia!";
pageobj.setPageText(text);
pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchWelcomes'));
pageobj.setCreateOption('recreate');
pageobj.save();
}
};
Twinkle.welcome.callback.evaluate = function friendlywelcomeCallbackEvaluate(e) {
var form = e.target;
var params = {
value: form.getChecked("template"),
article: form.article.value,
mode: 'manual'
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.welcome.callbacks.main);
};
/*
****************************************
*** twinklearv.js: ARV module
****************************************
* Mode of invocation: Tab ("ARV")
* Active on: Existing and non-existing user pages, user talk pages, contributions pages
* Config directives in: TwinkleConfig
*/
Twinkle.arv = function twinklearv() {
var username = Morebits.getPageAssociatedUser();
if ( username === false ) {
return;
}
var title = Morebits.isIPAddress( username ) ? 'Report IP to administrators' : 'Report user to administrators';
twAddPortletLink( function(){ Twinkle.arv.callback(username); }, "VIP", "tw-arv", title );
};
Twinkle.arv.callback = function ( uid ) {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
if ( uid === mw.config.get('wgUserName') ) {
alert( 'You don\'t want to report yourself, do you?' );
return;
}
var Window = new Morebits.simpleWindow( 600, 500 );
Window.setTitle( "Vandalism in progress" ); //changed title
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#arv" );
var form = new Morebits.quickForm( Twinkle.arv.callback.evaluate );
var categories = form.append( {
type: 'select',
name: 'category',
label: 'Select report type: ',
event: Twinkle.arv.callback.changeCategory
} );
categories.append( {
type: 'option',
label: 'Vandalism (WP:VIP)',
value: 'aiv'
} );
form.append( {
type: 'field',
label:'Work area',
name: 'work_area'
} );
form.append( { type:'submit' } );
form.append( {
type: 'hidden',
name: 'uid',
value: uid
} );
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.category.dispatchEvent( evt );
};
Twinkle.arv.callback.changeCategory = function (e) {
var value = e.target.value;
var root = e.target.form;
var old_area = Morebits.quickForm.getElements(root, "work_area")[0];
var work_area = null;
switch( value ) {
case 'aiv':
/* falls through */
default:
work_area = new Morebits.quickForm.element( {
type: 'field',
label: 'Report user for vandalism',
name: 'work_area'
} );
work_area.append( {
type: 'input',
name: 'page',
label: 'Primary linked page: ',
tooltip: 'Leave blank to not link to the page in the report',
value: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '',
event: function(e) {
var value = e.target.value;
var root = e.target.form;
if( value === '' ) {
root.badid.disabled = root.goodid.disabled = true;
} else {
root.badid.disabled = false;
root.goodid.disabled = root.badid.value === '';
}
}
} );
work_area.append( {
type: 'input',
name: 'badid',
label: 'Revision ID for target page when vandalised: ',
tooltip: 'Leave blank for no diff link',
value: Morebits.queryString.exists( 'vanarticlerevid' ) ? Morebits.queryString.get( 'vanarticlerevid' ) : '',
disabled: !Morebits.queryString.exists( 'vanarticle' ),
event: function(e) {
var value = e.target.value;
var root = e.target.form;
root.goodid.disabled = value === '';
}
} );
work_area.append( {
type: 'input',
name: 'goodid',
label: 'Last good revision ID before vandalism of target page: ',
tooltip: 'Leave blank for diff link to previous revision',
value: Morebits.queryString.exists( 'vanarticlegoodrevid' ) ? Morebits.queryString.get( 'vanarticlegoodrevid' ) : '',
disabled: !Morebits.queryString.exists( 'vanarticle' ) || Morebits.queryString.exists( 'vanarticlerevid' )
} );
work_area.append( {
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Vandalism after final (level 4 or 4im) warning given',
value: 'final'
},
{
label: 'Vandalism after recent (within 1 day) release of block',
value: 'postblock'
},
{
label: 'Evidently a vandalism-only account',
value: 'vandalonly',
disabled: Morebits.isIPAddress( root.uid.value )
},
{
label: 'Account is evidently a spambot or a compromised account',
value: 'spambot'
},
{
label: 'Account is a promotion-only account',
value: 'promoonly'
}
]
} );
work_area.append( {
type: 'textarea',
name: 'reason',
label: 'Comment: '
} );
work_area = work_area.render();
old_area.parentNode.replaceChild( work_area, old_area );
break;
}
};
Twinkle.arv.callback.evaluate = function(e) {
var form = e.target;
var reason = "";
var comment = "";
if ( form.reason ) {
comment = form.reason.value;
}
var uid = form.uid.value;
var types;
switch( form.category.value ) {
// Report user for vandalism
case 'aiv':
/* falls through */
default:
types = form.getChecked( 'arvtype' );
if( !types.length && comment === '' ) {
alert( 'You must specify some reason' );
return;
}
types = types.map( function(v) {
switch(v) {
case 'final':
return 'vandalism after final warning';
case 'postblock':
return 'vandalism after recent release of block';
case 'spambot':
return 'account is evidently a spambot or a compromised account';
case 'vandalonly':
return 'actions evidently indicate a vandalism-only account';
case 'promoonly':
return 'account is being used only for promotional purposes';
default:
return 'unknown reason';
}
} ).join( '; ' );
if ( form.page.value !== '' ) {
// add a leading : on linked page namespace to prevent transclusion
reason = 'On [[' + form.page.value.replace( /^(Image|Category|File):/i, ':$1:' ) + ']]';
if ( form.badid.value !== '' ) {
var query = {
'title': form.page.value,
'diff': form.badid.value,
'oldid': form.goodid.value
};
reason += ' ({{diff|' + form.page.value + '|' + form.badid.value + '|' + form.goodid.value + '|diff}})';
}
reason += ':';
}
if ( types ) {
reason += " " + types;
}
if (comment !== "" ) {
reason += (reason === "" ? "" : ". ") + comment;
}
reason += ". ~~~~";
reason = reason.replace(/\r?\n/g, "\n*:"); // indent newlines
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = "Wikipedia:Vandalism in progress";
Morebits.wiki.actionCompleted.notice = "Reporting complete";
var aivPage = new Morebits.wiki.page( 'Wikipedia:Vandalism in progress', 'Processing VIP request' );
aivPage.setPageSection( 3 );
aivPage.setFollowRedirect( true );
aivPage.load( function() {
var text = aivPage.getPageText();
// check if user has already been reported
if (new RegExp( "\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*" + RegExp.escape( uid, true ) + "\\s*\\}\\}" ).test(text)) {
aivPage.getStatusElement().info( 'Report already present, will not add a new one' );
return;
}
aivPage.getStatusElement().status( 'Adding new report...' );
aivPage.setEditSummary( 'Reporting [[Special:Contributions/' + uid + '|' + uid + ']].' + Twinkle.getPref('summaryAd') );
aivPage.setAppendText( '\n*{{' + ( Morebits.isIPAddress( uid ) ? 'IPvandal' : 'vandal' ) + '|' + (/\=/.test( uid ) ? '1=' : '' ) + uid + '}} – ' + reason );
aivPage.append();
} );
break;
}
};
/*
****************************************
*** twinklebatchdelete.js: Batch delete module (sysops only)
****************************************
* Mode of invocation: Tab ("D-batch")
* Active on: Existing and non-existing non-articles, and Special:PrefixIndex
* Config directives in: TwinkleConfig
*/
Twinkle.batchdelete = function twinklebatchdelete() {
if( Morebits.userIsInGroup( 'sysop' ) && (mw.config.get( 'wgNamespaceNumber' ) > 0 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) {
twAddPortletLink( Twinkle.batchdelete.callback, "D-batch", "tw-batch", "Delete pages found in this category/on this page" );
}
};
Twinkle.batchdelete.unlinkCache = {};
Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Batch deletion" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#batchdelete" );
var form = new Morebits.quickForm( Twinkle.batchdelete.callback.evaluate );
form.append( {
type: 'checkbox',
list: [
{
label: 'Delete pages',
name: 'delete_page',
value: 'delete',
checked: true
},
{
label: 'Remove backlinks to the page',
name: 'unlink_page',
value: 'unlink',
checked: false
},
{
label: 'Delete redirects to deleted pages',
name: 'delete_redirects',
value: 'delete_redirects',
checked: true
}
]
} );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: '
} );
var query;
if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // Category:
query = {
'action': 'query',
'generator': 'categorymembers',
'gcmtitle': mw.config.get( 'wgPageName' ),
'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': [ 'categories', 'revisions' ],
'rvprop': [ 'size' ]
};
} else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) {
var gapnamespace, gapprefix;
if(Morebits.queryString.exists( 'from' ) )
{
gapnamespace = Morebits.queryString.get( 'namespace' );
gapprefix = Morebits.string.toUpperCaseFirstChar( Morebits.queryString.get( 'from' ) );
}
else
{
var pathSplit = location.pathname.split('/');
if (pathSplit.length < 3 || pathSplit[2] !== "Special:PrefixIndex") {
return;
}
var titleSplit = pathSplit[3].split(':');
gapnamespace = mw.config.get("wgNamespaceIds")[titleSplit[0].toLowerCase()];
if ( titleSplit.length < 2 || typeof gapnamespace === 'undefined' )
{
gapnamespace = 0; // article namespace
gapprefix = pathSplit.splice(3).join('/');
}
else
{
pathSplit = pathSplit.splice(4);
pathSplit.splice(0,0,titleSplit.splice(1).join(':'));
gapprefix = pathSplit.join('/');
}
}
query = {
'action': 'query',
'generator': 'allpages',
'gapnamespace': gapnamespace ,
'gapprefix': gapprefix,
'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop' : ['categories', 'revisions' ],
'rvprop': [ 'size' ]
};
} else {
query = {
'action': 'query',
'generator': 'links',
'titles': mw.config.get( 'wgPageName' ),
'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': [ 'categories', 'revisions' ],
'rvprop': [ 'size' ]
};
}
var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//page[@ns != "6" and not(@missing)]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); // 6 = File: namespace
var list = [];
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var object = snapshot.snapshotItem(i);
var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue;
var size = xmlDoc.evaluate( 'revisions/rev/@size', object, null, XPathResult.NUMBER_TYPE, null ).numberValue;
var disputed = xmlDoc.evaluate( 'boolean(categories/cl[@title="Category:Contested candidates for speedy deletion"])', object, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue;
list.push( {label:page + ' (' + size + ' bytes)' + ( disputed ? ' (DISPUTED CSD)' : '' ), value:page, checked:!disputed });
}
self.params.form.append( {
type: 'checkbox',
name: 'pages',
list: list
} );
self.params.form.append( { type:'submit' } );
var result = self.params.form.render();
self.params.Window.setContent( result );
} );
wikipedia_api.params = { form:form, Window:Window };
wikipedia_api.post();
var root = document.createElement( 'div' );
Morebits.status.init( root );
Window.setContent( root );
Window.display();
};
Twinkle.batchdelete.currentDeleteCounter = 0;
Twinkle.batchdelete.currentUnlinkCounter = 0;
Twinkle.batchdelete.currentdeletor = 0;
Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Status';
Morebits.wiki.actionCompleted.postfix = 'batch deletion is now complete';
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
var delete_page = event.target.delete_page.checked;
var unlink_page = event.target.unlink_page.checked;
var delete_redirects = event.target.delete_redirects.checked;
if( ! reason ) {
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'nothing to delete, aborting' );
return;
}
function toCall( work ) {
if( work.length === 0 && Twinkle.batchdelete.currentDeleteCounter <= 0 && Twinkle.batchdelete.currentUnlinkCounter <= 0 ) {
window.clearInterval( Twinkle.batchdelete.currentdeletor );
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && ( Twinkle.batchdelete.currentDeleteCounter <= Twinkle.getPref('batchDeleteMinCutOff') || Twinkle.batchdelete.currentUnlinkCounter <= Twinkle.getPref('batchDeleteMinCutOff') ) ) {
Twinkle.batchdelete.unlinkCache = []; // Clear the cache
var pages = work.shift();
Twinkle.batchdelete.currentDeleteCounter += pages.length;
Twinkle.batchdelete.currentUnlinkCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var page = pages[i];
var query = {
'action': 'query',
'titles': page
};
var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchdelete.callbacks.main );
wikipedia_api.params = { page:page, reason:reason, unlink_page:unlink_page, delete_page:delete_page, delete_redirects:delete_redirects };
wikipedia_api.post();
}
}
}
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchdeleteChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchdelete.currentdeletor = window.setInterval( toCall, 1000, work );
};
Twinkle.batchdelete.callbacks = {
main: function( self ) {
var xmlDoc = self.responseXML;
var normal = xmlDoc.evaluate( '//normalized/n/@to', xmlDoc, null, XPathResult.STRING_TYPE, null ).stringValue;
if( normal ) {
self.params.page = normal;
}
var exists = xmlDoc.evaluate( 'boolean(//pages/page[not(@missing)])', xmlDoc, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue;
if( ! exists ) {
self.statelem.error( "It seems that the page doesn't exist, perhaps it has already been deleted" );
return;
}
var query, wikipedia_api;
if( self.params.unlink_page ) {
query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'nonredirects',
'blnamespace': [0, 100], // main space and portal space only
'bltitle': self.params.page,
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops
};
wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain );
wikipedia_api.params = self.params;
wikipedia_api.post();
} else {
--Twinkle.batchdelete.currentUnlinkCounter;
}
if( self.params.delete_page ) {
if (self.params.delete_redirects)
{
query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'redirects',
'bltitle': self.params.page,
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops
};
wikipedia_api = new Morebits.wiki.api( 'Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain );
wikipedia_api.params = self.params;
wikipedia_api.post();
}
var wikipedia_page = new Morebits.wiki.page( self.params.page, 'Deleting page ' + self.params.page );
wikipedia_page.setEditSummary(self.params.reason + Twinkle.getPref('deletionSummaryAd'));
wikipedia_page.deletePage(function( apiobj ) {
--Twinkle.batchdelete.currentDeleteCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl(self.params.page) );
link.setAttribute( 'title', self.params.page );
link.appendChild( document.createTextNode( self.params.page ) );
apiobj.statelem.info( [ 'completed (' , link , ')' ] );
} );
} else {
--Twinkle.batchdelete.currentDeleteCounter;
}
},
deleteRedirectsMain: function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
var total = snapshot.snapshotLength;
if( snapshot.snapshotLength === 0 ) {
return;
}
var statusIndicator = new Morebits.status('Deleting redirects for ' + self.params.page, '0%');
var onsuccess = function( self ) {
var obj = self.params.obj;
var total = self.params.total;
var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%';
obj.update( now );
self.statelem.unlink();
if( self.params.current >= total ) {
obj.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
if( snapshot.snapshotLength === 0 ) {
statusIndicator.info( '100% (completed)' );
Morebits.wiki.removeCheckpoint();
return;
}
var params = $.extend({}, self.params);
params.current = 0;
params.total = total;
params.obj = statusIndicator;
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var title = snapshot.snapshotItem(i).value;
var wikipedia_page = new Morebits.wiki.page( title, "Deleting " + title );
wikipedia_page.setEditSummary('[[WP:QD#G8|G8]]: Redirect to deleted page "' + self.params.page + '"' + Twinkle.getPref('deletionSummaryAd'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.deletePage(onsuccess);
}
},
unlinkBacklinksMain: function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
if( snapshot.snapshotLength === 0 ) {
--Twinkle.batchdelete.currentUnlinkCounter;
return;
}
var statusIndicator = new Morebits.status('Unlinking backlinks to ' + self.params.page, '0%');
var total = snapshot.snapshotLength * 2;
var onsuccess = function( self ) {
var obj = self.params.obj;
var total = self.params.total;
var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%';
obj.update( now );
self.statelem.unlink();
if( self.params.current >= total ) {
obj.info( now + ' (completed)' );
--Twinkle.batchdelete.currentUnlinkCounter;
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
if( snapshot.snapshotLength === 0 ) {
statusIndicator.info( '100% (completed)' );
--Twinkle.batchdelete.currentUnlinkCounter;
Morebits.wiki.removeCheckpoint();
return;
}
self.params.total = total;
self.params.obj = statusIndicator;
self.params.current = 0;
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var title = snapshot.snapshotItem(i).value;
var wikipedia_page = new Morebits.wiki.page( title, "Unlinking on " + title );
var params = $.extend( {}, self.params );
params.title = title;
params.onsuccess = onsuccess;
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.batchdelete.callbacks.unlinkBacklinks);
}
},
unlinkBacklinks: function( pageobj ) {
var params = pageobj.getCallbackParameters();
if( ! pageobj.exists() ) {
// we probably just deleted it, as a recursive backlink
params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } );
Morebits.wiki.actionCompleted();
return;
}
var text;
if( params.title in Twinkle.batchdelete.unlinkCache ) {
text = Twinkle.batchdelete.unlinkCache[ params.title ];
} else {
text = pageobj.getPageText();
}
var old_text = text;
var wikiPage = new Morebits.wikitext.page( text );
wikiPage.removeLink( params.page );
text = wikiPage.getText();
Twinkle.batchdelete.unlinkCache[ params.title ] = text;
if( text === old_text ) {
// Nothing to do, return
params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } );
Morebits.wiki.actionCompleted();
return;
}
pageobj.setEditSummary('Removing link(s) to deleted page ' + self.params.page + Twinkle.getPref('deletionSummaryAd'));
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.save(params.onsuccess);
}
};
/*
****************************************
*** twinklebatchprotect.js: Batch protect module (sysops only)
****************************************
* Mode of invocation: Tab ("P-batch")
* Active on: Existing project pages and user pages; existing and
* non-existing categories; Special:PrefixIndex
* Config directives in: TwinkleConfig
*/
Twinkle.batchprotect = function twinklebatchprotect() {
if( Morebits.userIsInGroup( 'sysop' ) && ((mw.config.get( 'wgArticleId' ) > 0 && (mw.config.get( 'wgNamespaceNumber' ) === 2 ||
mw.config.get( 'wgNamespaceNumber' ) === 4)) || mw.config.get( 'wgNamespaceNumber' ) === 14 ||
mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) {
twAddPortletLink( Twinkle.batchprotect.callback, "P-batch", "tw-pbatch", "Protect pages linked from this page" );
}
};
Twinkle.batchprotect.unlinkCache = {};
Twinkle.batchprotect.callback = function twinklebatchprotectCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Batch protection" );
Window.setScriptName( "Twinkle" );
//Window.addFooterLink( "Protection templates", "Template:Protection templates" );
Window.addFooterLink( "Protection policy", "WP:PROT" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#protect" );
var form = new Morebits.quickForm( Twinkle.batchprotect.callback.evaluate );
form.append({
type: 'checkbox',
name: 'editmodify',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: 'Modify edit protection',
value: 'editmodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
var editlevel = form.append({
type: 'select',
name: 'editlevel',
label: 'Edit protection:',
event: Twinkle.protect.formevents.editlevel
});
editlevel.append({
type: 'option',
label: 'All',
value: 'all'
});
editlevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
editlevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'editexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', selected: true, value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', value:'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append({
type: 'checkbox',
name: 'movemodify',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: 'Modify move protection',
value: 'movemodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
var movelevel = form.append({
type: 'select',
name: 'movelevel',
label: 'Move protection:',
event: Twinkle.protect.formevents.movelevel
});
movelevel.append({
type: 'option',
label: 'All',
value: 'all'
});
movelevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
movelevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'moveexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', selected: true, value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', value:'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append({
type: 'checkbox',
name: 'createmodify',
event: function twinklebatchprotectFormCreatemodifyEvent(e) {
e.target.form.createlevel.disabled = !e.target.checked;
e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all');
e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = (e.target.checked ? "" : "transparent");
},
list: [
{
label: 'Modify create protection',
value: 'createmodify',
tooltip: 'Only for pages that do not exist.',
checked: true
}
]
});
var createlevel = form.append({
type: 'select',
name: 'createlevel',
label: 'Create protection:',
event: Twinkle.protect.formevents.createlevel
});
createlevel.append({
type: 'option',
label: 'All',
value: 'all'
});
createlevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
createlevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'createexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', selected: true, value: 'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason (for protection log): '
} );
var query;
if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // categories
query = {
'action': 'query',
'generator': 'categorymembers',
'gcmtitle': mw.config.get( 'wgPageName' ),
'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': 'revisions',
'rvprop': 'size'
};
} else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) {
query = {
'action': 'query',
'generator': 'allpages',
'gapnamespace': Morebits.queryString.exists('namespace') ? Morebits.queryString.get( 'namespace' ) : document.getElementById('namespace').value,
'gapprefix': Morebits.queryString.exists('from') ? Morebits.string.toUpperCaseFirstChar(Morebits.queryString.get( 'from' ).replace('+', ' ')) :
Morebits.string.toUpperCaseFirstChar(document.getElementById('nsfrom').value),
'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': 'revisions',
'rvprop': 'size'
};
} else {
query = {
'action': 'query',
'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops
'generator': 'links',
'titles': mw.config.get( 'wgPageName' ),
'prop': 'revisions',
'rvprop': 'size'
};
}
var statusdiv = document.createElement("div");
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.status.init(statusdiv);
Window.display();
var statelem = new Morebits.status("Grabbing list of pages");
var wikipedia_api = new Morebits.wiki.api( 'loading...', query, function(apiobj) {
var xml = apiobj.responseXML;
var $pages = $(xml).find('page');
var list = [];
$pages.each(function(index, page) {
var $page = $(page);
var title = $page.attr('title');
var isRedir = $page.attr('redirect') === ""; // XXX ??
var missing = $page.attr('missing') === ""; // XXX ??
var size = $page.find('rev').attr('size');
var metadata = [];
if (missing) {
metadata.push("page does not exist");
} else {
if (isRedir) {
metadata.push("redirect");
}
metadata.push(size + " bytes");
}
list.push( { label: title + (metadata.length ? (' (' + metadata.join('; ') + ')') : '' ), value: title, checked: true });
});
form.append({ type: 'header', label: 'Pages to protect' });
form.append( {
type: 'checkbox',
name: 'pages',
list: list
} );
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
}, statelem );
wikipedia_api.post();
};
Twinkle.batchprotect.currentProtectCounter = 0;
Twinkle.batchprotect.currentprotector = 0;
Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) {
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
var editmodify = event.target.editmodify.checked;
var editlevel = event.target.editlevel.value;
var editexpiry = event.target.editexpiry.value;
var movemodify = event.target.movemodify.checked;
var movelevel = event.target.movelevel.value;
var moveexpiry = event.target.moveexpiry.value;
var createmodify = event.target.createmodify.checked;
var createlevel = event.target.createlevel.value;
var createexpiry = event.target.createexpiry.value;
if( ! reason ) {
alert("You've got to give a reason, you rouge admin!");
return;
}
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'Nothing to protect, aborting' );
return;
}
var toCall = function twinklebatchprotectToCall( work ) {
if( work.length === 0 && Twinkle.batchprotect.currentProtectCounter <= 0 ) {
Morebits.status.info( 'work done' );
window.clearInterval( Twinkle.batchprotect.currentprotector );
Twinkle.batchprotect.currentprotector = Twinkle.batchprotect.currentProtectCounter = 0;
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && Twinkle.batchprotect.currentProtectCounter <= Twinkle.getPref('batchProtectMinCutOff') ) {
var pages = work.shift();
Twinkle.batchprotect.currentProtectCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var page = pages[i];
var query = {
'action': 'query',
'titles': page
};
var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchprotect.callbacks.main );
wikipedia_api.params = {
page: page,
reason: reason,
editmodify: editmodify,
editlevel: editlevel,
editexpiry: editexpiry,
movemodify: movemodify,
movelevel: movelevel,
moveexpiry: moveexpiry,
createmodify: createmodify,
createlevel: createlevel,
createexpiry: createexpiry
};
wikipedia_api.post();
}
}
};
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchProtectChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchprotect.currentprotector = window.setInterval( toCall, 1000, work );
};
Twinkle.batchprotect.callbacks = {
main: function( apiobj ) {
var xml = apiobj.responseXML;
var normal = $(xml).find('normalized n').attr('to');
if( normal ) {
apiobj.params.page = normal;
}
var exists = ($(xml).find('page').attr('missing') !== "");
var page = new Morebits.wiki.page(apiobj.params.page, "Protecting " + apiobj.params.page);
var takenAction = false;
if (exists && apiobj.params.editmodify) {
page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry);
takenAction = true;
}
if (exists && apiobj.params.movemodify) {
page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry);
takenAction = true;
}
if (!exists && apiobj.params.createmodify) {
page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry);
takenAction = true;
}
if (!takenAction) {
Morebits.status.warn("Protecting " + apiobj.params.page, "page " + (exists ? "exists" : "does not exist") + "; nothing to do, skipping");
return;
}
page.setEditSummary(apiobj.params.reason);
page.protect(function(pageobj) {
--Twinkle.batchprotect.currentProtectCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl( apiobj.params.page ) );
link.appendChild( document.createTextNode( apiobj.params.page ) );
pageobj.getStatusElement().info( [ 'completed (' , link , ')' ] );
} );
}
};
/*
****************************************
*** twinklebatchundelete.js: Batch undelete module
****************************************
* Mode of invocation: Tab ("Und-batch")
* Active on: Existing and non-existing user pages (??? why?)
* Config directives in: TwinkleConfig
*/
// XXX TODO this module needs to be overhauled to use Morebits.wiki.page
Twinkle.batchundelete = function twinklebatchundelete() {
if( mw.config.get("wgNamespaceNumber") !== mw.config.get("wgNamespaceIds").user ) {
return;
}
if( Morebits.userIsInGroup( 'sysop' ) ) {
twAddPortletLink( Twinkle.batchundelete.callback, "Und-batch", "tw-batch-undel", "Undelete 'em all" );
}
};
Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setScriptName("Twinkle");
Window.setTitle("Batch undelete")
var form = new Morebits.quickForm( Twinkle.batchundelete.callback.evaluate );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: '
} );
var query = {
'action': 'query',
'generator': 'links',
'titles': mw.config.get("wgPageName"),
'gpllimit' : Twinkle.getPref('batchMax') // the max for sysops
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//page[@missing]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
var list = [];
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var object = snapshot.snapshotItem(i);
var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue;
list.push( {label:page, value:page, checked: true });
}
self.params.form.append( {
type: 'checkbox',
name: 'pages',
list: list
}
);
self.params.form.append( { type:'submit' } );
var result = self.params.form.render();
self.params.Window.setContent( result );
} );
wikipedia_api.params = { form:form, Window:Window };
wikipedia_api.post();
var root = document.createElement( 'div' );
Morebits.status.init( root );
Window.setContent( root );
Window.display();
};
Twinkle.batchundelete.currentUndeleteCounter = 0;
Twinkle.batchundelete.currentundeletor = 0;
Twinkle.batchundelete.callback.evaluate = function( event ) {
Morebits.wiki.actionCompleted.notice = 'Status';
Morebits.wiki.actionCompleted.postfix = 'batch undeletion is now completed';
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
if( ! reason ) {
alert("You need to give a reason, you cabal crony!");
return;
}
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'nothing to undelete, aborting' );
return;
}
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchUndeleteChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchundelete.currentundeletor = window.setInterval( Twinkle.batchundelete.callbacks.main, 1000, work, reason );
};
Twinkle.batchundelete.callbacks = {
main: function( work, reason ) {
if( work.length === 0 && Twinkle.batchundelete.currentUndeleteCounter <= 0 ) {
Morebits.status.info( 'work done' );
window.clearInterval( Twinkle.batchundelete.currentundeletor );
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && Twinkle.batchundelete.currentUndeleteCounter <= Twinkle.getPref('batchUndeleteMinCutOff') ) {
var pages = work.shift();
Twinkle.batchundelete.currentUndeleteCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var title = pages[i];
var query = {
'token': mw.user.tokens.get().editToken,
'title': title,
'action': 'undelete',
'reason': reason + Twinkle.getPref('deletionSummaryAd')
};
var wikipedia_api = new Morebits.wiki.api( "Undeleting " + title, query, function( self ) {
--Twinkle.batchundelete.currentUndeleteCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl(self.itsTitle) );
link.setAttribute( 'title', self.itsTitle );
link.appendChild( document.createTextNode(self.itsTitle) );
self.statelem.info( ['completed (',link,')'] );
});
wikipedia_api.itsTitle = title;
wikipedia_api.post();
}
}
}
};
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences and user
subpages named "/Twinkle preferences", and adds ad box to the top of user
subpages belonging to the currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
* Config directives in: TwinkleConfig
I, [[User:This, that and the other]], originally wrote this. If the code is misbehaving, or you have any
questions, don't hesitate to ask me. (This doesn't at all imply [[WP:OWN]]ership - it's just meant to
point you in the right direction.) -- TTO
*/
Twinkle.config = {};
Twinkle.config.commonEnums = {
watchlist: { yes: "Add to watchlist", no: "Don't add to watchlist", "default": "Follow your site preferences" },
talkPageMode: { window: "In a window, replacing other user talks", tab: "In a new tab", blank: "In a totally new window" }
};
Twinkle.config.commonSets = {
csdCriteria: {
db: "Custom rationale"
},
csdCriteriaDisplayOrder: [
"db"
],
csdCriteriaNotificationDisplayOrder: [
"db"
],
csdAndDICriteria: {
db: "Custom rationale"
},
csdAndDICriteriaDisplayOrder: [
"db"
],
namespacesNoSpecial: {
"0": "Article",
"1": "Talk (article)",
"2": "User",
"3": "User talk",
"4": "Wikipedia",
"5": "Wikipedia talk",
"6": "File",
"7": "File talk",
"8": "MediaWiki",
"9": "MediaWiki talk",
"10": "Template",
"11": "Template talk",
"12": "Help",
"13": "Help talk",
"14": "Category",
"15": "Category talk"
}
};
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* inFriendlyConfig: <true for preferences located under FriendlyConfig rather than TwinkleConfig>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: "General",
preferences: [
// TwinkleConfig.summaryAd (string)
// Text to be appended to the edit summary of edits made using Twinkle
{
name: "summaryAd",
label: "\"Ad\" to be appended to Twinkle's edit summaries",
helptip: "The summary ad should start with a space, and be kept short.",
type: "string"
},
// TwinkleConfig.deletionSummaryAd (string)
// Text to be appended to the edit summary of deletions made using Twinkle
{
name: "deletionSummaryAd",
label: "Summary ad to use for deletion summaries",
helptip: "Normally the same as the edit summary ad above.",
adminOnly: true,
type: "string"
},
// TwinkleConfig.protectionSummaryAd (string)
// Text to be appended to the edit summary of page protections made using Twinkle
{
name: "protectionSummaryAd",
label: "Summary ad to use for page protections",
helptip: "Normally the same as the edit summary ad above.",
adminOnly: true,
type: "string"
},
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: "userTalkPageMode",
label: "When opening a user talk page, open it",
type: "enum",
enumValues: Twinkle.config.commonEnums.talkPageMode
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: "dialogLargeFont",
label: "Use larger text in Twinkle dialogs",
type: "boolean"
}
]
},
{
title: "Revert and rollback", // twinklefluff module
preferences: [
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: "openTalkPage",
label: "Open user talk page after these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contrib page, because from there, actions may be multiple, and opening talk page not suitable. If set to true, openTalkPage defines then if talk page will be opened.
{
name: "openTalkPageOnAutoRevert",
label: "Open user talk page when invoking rollback from user contributions",
helptip: "Often, you may be rolling back many pages at a time from a vandal's contributions page, so it would be unsuitable to open the user talk page. Hence, this option is off by default. When this is on, the desired options must be enabled in the previous setting for this to work.",
type: "boolean"
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: "markRevertedPagesAsMinor",
label: "Mark as minor edit for these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: "watchRevertedPages",
label: "Add pages to watchlist for these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: "offerReasonOnNormalRevert",
label: "Prompt for reason for normal rollbacks",
helptip: "\"Normal\" rollbacks are the ones that are invoked from the middle [rollback] link.",
type: "boolean"
},
{
name: "confirmOnFluff",
label: "Provide a confirmation message before reverting",
helptip: "For users of pen or touch devices, and chronically indecisive people.",
type: "boolean"
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links (diff, others, mine, contribs)
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: "showRollbackLinks",
label: "Show rollback links on these pages",
type: "set",
setValues: { diff: "Diff pages", others: "Contributions pages of other users", mine: "My contributions page" }
}
]
},
{
title: "Deletion tagging",
preferences: [
{
name: "speedySelectionStyle",
label: "When to go ahead and tag/delete the page",
type: "enum",
enumValues: { "buttonClick": 'When I click "Submit"', "radioClick": "As soon as I click an option" }
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as patrolled (if the page was reached from NewPages)
{
name: "markSpeedyPagesAsPatrolled",
label: "Mark page as patrolled when tagging (if possible)",
helptip: "Due to technical limitations, pages are only marked as patrolled when they are reached via Special:NewPages.",
type: "boolean"
},
// TwinkleConfig.openUserTalkPageOnSpeedyDelete (array of strings)
// What types of actions that should result user talk page to be opened when speedily deleting (admin only)
{
name: "openUserTalkPageOnSpeedyDelete",
label: "Open user talk page when deleting under these criteria",
adminOnly: true,
type: "set",
setValues: Twinkle.config.commonSets.csdAndDICriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndDICriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: "deleteTalkPageOnDelete",
label: "Check the \"also delete talk page\" box by default",
adminOnly: true,
type: "boolean"
},
// TwinkleConfig.deleteSysopDefaultToTag (boolean)
// Make the CSD screen default to "tag" instead of "delete" (admin only)
{
name: "deleteSysopDefaultToTag",
label: "Default to tagging instead of outright deletion",
adminOnly: true,
type: "boolean"
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: "speedyWindowWidth",
label: "Width of deletion window (pixels)",
type: "integer"
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: "speedyWindowHeight",
label: "Height of deletion window (pixels)",
helptip: "If you have a big monitor, you might like to increase this.",
type: "integer"
},
{
name: "logSpeedyNominations",
label: "Keep a log in userspace of all deletion nominations",
helptip: "Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you nominate for QD using Twinkle. Files tagged using DI are also added to this log.",
type: "boolean"
},
{
name: "speedyLogPageName",
label: "Keep the deletion userspace log at this user subpage",
helptip: "i.e. User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the deletion userspace log.",
type: "string"
}
]
},
{
title: "Unlink",
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article) and 100 (portal)
{
name: "unlinkNamespaces",
label: "Remove links from pages in these namespaces",
helptip: "Avoid selecting any talk namespaces, as Twinkle might end up unlinking on talk archives (a big no-no).",
type: "set",
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: "Hidden",
hidden: true,
preferences: [
// twinkle.header.js: portlet setup
{
name: "portletArea",
type: "string"
},
{
name: "portletId",
type: "string"
},
{
name: "portletName",
type: "string"
},
{
name: "portletType",
type: "string"
},
{
name: "portletNext",
type: "string"
},
// twinklefluff.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: "revertMaxRevisions",
type: "integer"
},
// twinklebatchdelete.js: How many pages should be processed at a time
{
name: "batchdeleteChunks",
type: "integer"
},
// twinklebatchdelete.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchDeleteMinCutOff",
type: "integer"
},
// twinklebatchdelete.js: How many pages should be processed maximum
{
name: "batchMax",
type: "integer"
},
// twinklebatchprotect.js: How many pages should be processed at a time
{
name: "batchProtectChunks",
type: "integer"
},
// twinklebatchprotect.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchProtectMinCutOff",
type: "integer"
},
// twinklebatchundelete.js: How many pages should be processed at a time
{
name: "batchundeleteChunks",
type: "integer"
},
// twinklebatchundelete.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchUndeleteMinCutOff",
type: "integer"
}
]
}
]; // end of Twinkle.config.sections
//{
// name: "",
// label: "",
// type: ""
// },
Twinkle.config.init = function twinkleconfigInit() {
if (( mw.config.get("wgTitle") === "Twinkle/Preferences" ||
(mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user && mw.config.get("wgTitle").lastIndexOf("/Twinkle preferences") === (mw.config.get("wgTitle").length - 20))) &&
mw.config.get("wgAction") === "view") {
// create the config page at Wikipedia:Twinkle/Preferences, and at user subpages (for testing purposes)
if (!document.getElementById("twinkle-config")) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style (the url() CSS function doesn't seem to work from wikicode - ?!)
document.getElementById("twinkle-config-titlebar").style.backgroundImage = "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC)";
var contentdiv = document.getElementById("twinkle-config-content");
contentdiv.textContent = ""; // clear children
// let user know about possible conflict with monobook.js/vector.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
var contentnotice = document.createElement("p");
// I hate innerHTML, but this is one thing it *is* good for...
contentnotice.innerHTML = "<b>Before modifying your preferences here,</b> make sure you have removed any old <code>TwinkleConfig</code> and <code>FriendlyConfig</code> settings from your <a href=\"" + mw.util.getUrl("Special:MyPage/skin.js") + "\" title=\"Special:MyPage/skin.js\">user JavaScript file</a>.";
contentdiv.appendChild(contentnotice);
// look and see if the user does in fact have any old settings in their skin JS file
var skinjs = new Morebits.wiki.page("User:" + mw.config.get("wgUserName") + "/" + mw.config.get("skin") + ".js");
skinjs.setCallbackParameters(contentnotice);
skinjs.load(Twinkle.config.legacyPrefsNotice);
// start a table of contents
var toctable = document.createElement("table");
toctable.className = "toc";
toctable.style.marginLeft = "0.4em";
var toctr = document.createElement("tr");
var toctd = document.createElement("td");
// create TOC title
var toctitle = document.createElement("div");
toctitle.id = "toctitle";
var toch2 = document.createElement("h2");
toch2.textContent = "Contents ";
toctitle.appendChild(toch2);
// add TOC show/hide link
var toctoggle = document.createElement("span");
toctoggle.className = "toctoggle";
toctoggle.appendChild(document.createTextNode("["));
var toctogglelink = document.createElement("a");
toctogglelink.className = "internal";
toctogglelink.setAttribute("href", "#tw-tocshowhide");
toctogglelink.textContent = "hide";
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode("]"));
toctitle.appendChild(toctoggle);
toctd.appendChild(toctitle);
// create item container: this is what we add stuff to
var tocul = document.createElement("ul");
toctogglelink.addEventListener("click", function twinkleconfigTocToggle() {
var $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(":visible").length) {
toctogglelink.textContent = "hide";
} else {
toctogglelink.textContent = "show";
}
}, false);
toctd.appendChild(tocul);
toctr.appendChild(toctd);
toctable.appendChild(toctr);
contentdiv.appendChild(toctable);
var tocnumber = 1;
var contentform = document.createElement("form");
contentform.setAttribute("action", "javascript:void(0)"); // was #tw-save - changed to void(0) to work around Chrome issue
contentform.addEventListener("submit", Twinkle.config.save, true);
contentdiv.appendChild(contentform);
var container = document.createElement("table");
container.style.width = "100%";
contentform.appendChild(container);
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // i.e. "continue" in this context
}
var configgetter; // retrieve the live config values
if (section.inFriendlyConfig) {
configgetter = Twinkle.getFriendlyPref;
} else {
configgetter = Twinkle.getPref;
}
// add to TOC
var tocli = document.createElement("li");
tocli.className = "toclevel-1";
var toca = document.createElement("a");
toca.setAttribute("href", "#twinkle-config-section-" + tocnumber.toString());
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
var row = document.createElement("tr");
var cell = document.createElement("td");
cell.setAttribute("colspan", "3");
var heading = document.createElement("h4");
heading.style.borderBottom = "1px solid gray";
heading.style.marginTop = "0.2em";
heading.id = "twinkle-config-section-" + (tocnumber++).toString();
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
var rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each(function(prefkey, pref) {
if (pref.adminOnly && !Morebits.userIsInGroup("sysop")) {
return true; // i.e. "continue" in this context
}
row = document.createElement("tr");
row.style.marginBottom = "0.2em";
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = "rgba(128, 128, 128, 0.1)";
}
cell = document.createElement("td");
var label, input;
switch (pref.type) {
case "boolean": // create a checkbox
cell.setAttribute("colspan", "2");
label = document.createElement("label");
input = document.createElement("input");
input.setAttribute("type", "checkbox");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
if (configgetter(pref.name) === true) {
input.setAttribute("checked", "checked");
}
label.appendChild(input);
label.appendChild(document.createTextNode(" " + pref.label));
cell.appendChild(label);
break;
case "string": // create an input box
case "integer":
// add label to first column
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
if (pref.type === "integer") {
input.setAttribute("size", 6);
input.setAttribute("type", "number");
input.setAttribute("step", "1"); // integers only
}
if (configgetter(pref.name)) {
input.setAttribute("value", configgetter(pref.name));
}
cell.appendChild(input);
break;
case "enum": // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
input = document.createElement("select");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
$.each(pref.enumValues, function(enumvalue, enumdisplay) {
var option = document.createElement("option");
option.setAttribute("value", enumvalue);
if (configgetter(pref.name) === enumvalue) {
option.setAttribute("selected", "selected");
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case "set": // create a set of check boxes
// add label first of all
cell.setAttribute("colspan", "2");
label = document.createElement("label"); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
var checkdiv = document.createElement("div");
checkdiv.style.paddingLeft = "1em";
var worker = function(itemkey, itemvalue) {
var checklabel = document.createElement("label");
checklabel.style.marginRight = "0.7em";
checklabel.style.display = "inline-block";
var check = document.createElement("input");
check.setAttribute("type", "checkbox");
check.setAttribute("id", pref.name + "_" + itemkey);
check.setAttribute("name", pref.name + "_" + itemkey);
if (configgetter(pref.name) && configgetter(pref.name).indexOf(itemkey) !== -1) {
check.setAttribute("checked", "checked");
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === "unlinkNamespaces") {
if (configgetter(pref.name) && configgetter(pref.name).indexOf(parseInt(itemkey, 10)) !== -1) {
check.setAttribute("checked", "checked");
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, function(itemkey, item) {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case "customList":
// add label to first column
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
var button = document.createElement("button");
button.setAttribute("id", pref.name);
button.setAttribute("name", pref.name);
button.setAttribute("type", "button");
button.addEventListener("click", Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: configgetter(pref.name),
pref: pref,
inFriendlyConfig: section.inFriendlyConfig
});
button.appendChild(document.createTextNode("Edit items"));
cell.appendChild(button);
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement("td");
cell.style.fontSize = "90%";
cell.style.color = "gray";
if (pref.helptip) {
cell.innerHTML = pref.helptip;
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== "customList") {
var resetlink = document.createElement("a");
resetlink.setAttribute("href", "#tw-reset");
resetlink.setAttribute("id", "twinkle-config-reset-" + pref.name);
resetlink.addEventListener("click", Twinkle.config.resetPrefLink, false);
if (resetlink.style.styleFloat) { // IE (inc. IE9)
resetlink.style.styleFloat = "right";
} else { // standards
resetlink.style.cssFloat = "right";
}
resetlink.style.margin = "0 0.6em";
resetlink.appendChild(document.createTextNode("Reset"));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
var footerbox = document.createElement("div");
footerbox.setAttribute("id", "twinkle-config-buttonpane");
footerbox.style.backgroundColor = "#BCCADF";
footerbox.style.padding = "0.5em";
var button = document.createElement("button");
button.setAttribute("id", "twinkle-config-submit");
button.setAttribute("type", "submit");
button.appendChild(document.createTextNode("Save changes"));
footerbox.appendChild(button);
var footerspan = document.createElement("span");
footerspan.className = "plainlinks";
footerspan.style.marginLeft = "2.4em";
footerspan.style.fontSize = "90%";
var footera = document.createElement("a");
footera.setAttribute("href", "#tw-reset-all");
footera.setAttribute("id", "twinkle-config-resetall");
footera.addEventListener("click", Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode("Restore defaults"));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (location.hash) {
location.hash = location.hash;
}
} else if (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user) {
var box = document.createElement("div");
box.setAttribute("id", "twinkle-config-headerbox");
box.style.border = "1px #f60 solid";
box.style.background = "#fed";
box.style.padding = "0.6em";
box.style.margin = "0.5em auto";
box.style.textAlign = "center";
var link;
if (mw.config.get("wgTitle") === mw.config.get("wgUserName") + "/twinkleoptions.js") {
// place "why not try the preference panel" notice
box.style.fontWeight = "bold";
box.style.width = "80%";
box.style.borderWidth = "2px";
if (mw.config.get("wgArticleId") > 0) { // page exists
box.appendChild(document.createTextNode("This page contains your Twinkle preferences. You can change them using the "));
} else { // page does not exist
box.appendChild(document.createTextNode("You can customize Twinkle to suit your preferences by using the "));
}
link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") );
link.appendChild(document.createTextNode("Twinkle preferences panel"));
box.appendChild(link);
box.appendChild(document.createTextNode(", or by editing this page."));
$(box).insertAfter($("#contentSub"));
} else if (mw.config.get("wgTitle").indexOf(mw.config.get("wgUserName")) === 0 &&
mw.config.get("wgPageName").lastIndexOf(".js") === mw.config.get("wgPageName").length - 3) {
// place "Looking for Twinkle options?" notice
box.style.width = "60%";
box.appendChild(document.createTextNode("If you want to set Twinkle preferences, you can use the "));
link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") );
link.appendChild(document.createTextNode("Twinkle preferences panel"));
box.appendChild(link);
box.appendChild(document.createTextNode("."));
$(box).insertAfter($("#contentSub"));
}
}
};
// Morebits.wiki.page callback from init code
Twinkle.config.legacyPrefsNotice = function twinkleconfigLegacyPrefsNotice(pageobj) {
var text = pageobj.getPageText();
var contentnotice = pageobj.getCallbackParameters();
if (text.indexOf("TwinkleConfig") !== -1 || text.indexOf("FriendlyConfig") !== -1) {
contentnotice.innerHTML = '<table class="plainlinks ombox ombox-content"><tr><td class="mbox-image">' +
'<img alt="" src="http://upload.wikimedia.org/wikipedia/en/3/38/Imbox_content.png" /></td>' +
'<td class="mbox-text"><p><big><b>Before modifying your settings here,</b> you must remove your old Twinkle and Friendly settings from your personal skin JavaScript.</big></p>' +
'<p>To do this, you can <a href="' + mw.config.get("wgScript") + '?title=User:' + encodeURIComponent(mw.config.get("wgUserName")) + '/' + mw.config.get("skin") + '.js&action=edit" target="_tab"><b>edit your personal JavaScript</b></a>, removing all lines of code that refer to <code>TwinkleConfig</code> and <code>FriendlyConfig</code>.</p>' +
'</td></tr></table>';
} else {
$(contentnotice).remove();
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow(dlgtable, value, label) {
var contenttr = document.createElement("tr");
// "remove" button
var contenttd = document.createElement("td");
var removeButton = document.createElement("button");
removeButton.setAttribute("type", "button");
removeButton.addEventListener("click", function() { $(contenttr).remove(); }, false);
removeButton.textContent = "Remove";
contenttd.appendChild(removeButton);
contenttr.appendChild(contenttd);
// value input box
contenttd = document.createElement("td");
var input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "twinkle-config-customlist-value";
input.style.width = "97%";
if (value) {
input.setAttribute("value", value);
}
contenttd.appendChild(input);
contenttr.appendChild(contenttd);
// label input box
contenttd = document.createElement("td");
input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "twinkle-config-customlist-label";
input.style.width = "98%";
if (label) {
input.setAttribute("value", label);
}
contenttd.appendChild(input);
contenttr.appendChild(contenttd);
dlgtable.appendChild(contenttr);
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
var $prefbutton = $(e.target);
var curvalue = $prefbutton.data("value");
var curpref = $prefbutton.data("pref");
var dialog = new Morebits.simpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName("Twinkle preferences");
var dialogcontent = document.createElement("div");
var dlgtable = document.createElement("table");
dlgtable.className = "wikitable";
dlgtable.style.margin = "1.4em 1em";
dlgtable.style.width = "auto";
var dlgtbody = document.createElement("tbody");
// header row
var dlgtr = document.createElement("tr");
// top-left cell
var dlgth = document.createElement("th");
dlgth.style.width = "5%";
dlgtr.appendChild(dlgth);
// value column header
dlgth = document.createElement("th");
dlgth.style.width = "35%";
dlgth.textContent = (curpref.customListValueTitle ? curpref.customListValueTitle : "Value");
dlgtr.appendChild(dlgth);
// label column header
dlgth = document.createElement("th");
dlgth.style.width = "60%";
dlgth.textContent = (curpref.customListLabelTitle ? curpref.customListLabelTitle : "Label");
dlgtr.appendChild(dlgth);
dlgtbody.appendChild(dlgtr);
// content rows
var gotRow = false;
$.each(curvalue, function(k, v) {
gotRow = true;
Twinkle.config.listDialog.addRow(dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow(dlgtbody);
}
// final "add" button
var dlgtfoot = document.createElement("tfoot");
dlgtr = document.createElement("tr");
var dlgtd = document.createElement("td");
dlgtd.setAttribute("colspan", "3");
var addButton = document.createElement("button");
addButton.style.minWidth = "8em";
addButton.setAttribute("type", "button");
addButton.addEventListener("click", function(e) {
Twinkle.config.listDialog.addRow(dlgtbody);
}, false);
addButton.textContent = "Add";
dlgtd.appendChild(addButton);
dlgtr.appendChild(dlgtd);
dlgtfoot.appendChild(dlgtr);
dlgtable.appendChild(dlgtbody);
dlgtable.appendChild(dlgtfoot);
dialogcontent.appendChild(dlgtable);
// buttonpane buttons: [Save changes] [Reset] [Cancel]
var button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
Twinkle.config.listDialog.save($prefbutton, dlgtbody);
dialog.close();
}, false);
button.textContent = "Save changes";
dialogcontent.appendChild(button);
button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
Twinkle.config.listDialog.reset($prefbutton, dlgtbody);
}, false);
button.textContent = "Reset";
dialogcontent.appendChild(button);
button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
dialog.close(); // the event parameter on this function seems to be broken
}, false);
button.textContent = "Cancel";
dialogcontent.appendChild(button);
dialog.setContent(dialogcontent);
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset(button, tbody) {
// reset value on button
var $button = $(button);
var curpref = $button.data("pref");
var oldvalue = $button.data("value");
Twinkle.config.resetPref(curpref, $button.data("inFriendlyConfig"));
// reset form
var $tbody = $(tbody);
$tbody.find("tr").slice(1).remove(); // all rows except the first (header) row
// add the new values
var curvalue = $button.data("value");
$.each(curvalue, function(k, v) {
Twinkle.config.listDialog.addRow(tbody, v.value, v.label);
});
// save the old value
$button.data("value", oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave(button, tbody) {
var result = [];
var current = {};
$(tbody).find('input[type="text"]').each(function(inputkey, input) {
if ($(input).hasClass("twinkle-config-customlist-value")) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$(button).data("value", result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
var wantedpref = e.target.id.substring(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // continue: skip impossibilities
}
var foundit = false;
$(section.preferences).each(function(prefkey, pref) {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref, section.inFriendlyConfig);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref, inFriendlyConfig) {
switch (pref.type) {
case "boolean":
document.getElementById(pref.name).checked = (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]);
break;
case "string":
case "integer":
case "enum":
document.getElementById(pref.name).value = (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]);
break;
case "set":
$.each(pref.setValues, function(itemkey, itemvalue) {
if (document.getElementById(pref.name + "_" + itemkey)) {
document.getElementById(pref.name + "_" + itemkey).checked = ((inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]).indexOf(itemkey) !== -1);
}
});
break;
case "customList":
$(document.getElementById(pref.name)).data("value", (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]));
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // continue: skip impossibilities
}
$(section.preferences).each(function(prefkey, pref) {
if (!pref.adminOnly || Morebits.userIsInGroup("sysop")) {
Twinkle.config.resetPref(pref, section.inFriendlyConfig);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.status.init( document.getElementById("twinkle-config-content") );
Morebits.wiki.actionCompleted.notice = "Save";
var userjs = mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user] + ":" + mw.config.get("wgUserName") + "/twinkleoptions.js";
var wikipedia_page = new Morebits.wiki.page(userjs, "Saving preferences to " + userjs);
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
var form = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
// this is the object which gets serialized into JSON
var newConfig = {
twinkle: {},
friendly: {}
};
// keeping track of all preferences that we encounter
// any others that are set in the user's current config are kept
// this way, preferences that this script doesn't know about are not lost
// (it does mean obsolete prefs will never go away, but... ah well...)
var foundTwinklePrefs = [], foundFriendlyPrefs = [];
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
var compare = function(a, b) {
if ($.isArray(a)) {
if (a.length !== b.length) {
return false;
}
var asort = a.sort(), bsort = b.sort();
for (var i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === "object") && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
} else {
return a === b;
}
};
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.adminOnly && !Morebits.userIsInGroup("sysop")) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each(function(prefkey, pref) {
var userValue; // = undefined
// only read form values for those prefs that have them
if (!section.hidden && (!pref.adminOnly || Morebits.userIsInGroup("sysop"))) {
switch (pref.type) {
case "boolean": // read from the checkbox
userValue = form[pref.name].checked;
break;
case "string": // read from the input box or combo box
case "enum":
userValue = form[pref.name].value;
break;
case "integer": // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.status.warn("Saving", "The value you specified for " + pref.name + " (" + pref.value + ") was invalid. The save will continue, but the invalid data value will be skipped.");
userValue = null;
}
break;
case "set": // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, function(itemkey, item) {
if (form[pref.name + "_" + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, function(itemkey, itemvalue) {
if (form[pref.name + "_" + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case "customList": // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data("value");
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
}
// only save those preferences that are *different* from the default
if (section.inFriendlyConfig) {
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.friendly[pref.name])) {
newConfig.friendly[pref.name] = userValue;
}
foundFriendlyPrefs.push(pref.name);
} else {
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.twinkle[pref.name])) {
newConfig.twinkle[pref.name] = userValue;
}
foundTwinklePrefs.push(pref.name);
}
});
});
if (Twinkle.prefs) {
$.each(Twinkle.prefs.twinkle, function(tkey, tvalue) {
if (foundTwinklePrefs.indexOf(tkey) === -1) {
newConfig.twinkle[tkey] = tvalue;
}
});
$.each(Twinkle.prefs.friendly, function(fkey, fvalue) {
if (foundFriendlyPrefs.indexOf(fkey) === -1) {
newConfig.friendly[fkey] = fvalue;
}
});
}
var text =
"// twinkleoptions.js: personal Twinkle preferences file\n" +
"//\n" +
"// NOTE: The easiest way to change your Twinkle preferences is by using the\n" +
"// Twinkle preferences panel, at [[" + mw.config.get("wgPageName") + "]].\n" +
"//\n" +
"// This file is AUTOMATICALLY GENERATED. Any changes you make (aside from\n" +
"// changing the configuration parameters in a valid-JavaScript way) will be\n" +
"// overwritten the next time you click \"save\" in the Twinkle preferences\n" +
"// panel. If modifying this file, make sure to use correct JavaScript.\n" +
"\n" +
"window.Twinkle.prefs = ";
text += JSON.stringify(newConfig, null, 2);
text +=
";\n" +
"\n" +
"// End of twinkleoptions.js\n";
pageobj.setPageText(text);
pageobj.setEditSummary("Saving Twinkle preferences: automatic edit from [[" + mw.config.get("wgPageName") + "]]");
pageobj.setCreateOption("recreate");
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info("successful");
var noticebox = document.createElement("div");
noticebox.className = "successbox";
noticebox.style.fontSize = "100%";
noticebox.style.marginTop = "2em";
noticebox.innerHTML = "<p><b>Your Twinkle preferences have been saved.</b></p><p>To see the changes, you will need to <b>clear your browser cache entirely</b> (see <a href=\"" + mw.util.getUrl("WP:BYPASS") + "\" title=\"WP:BYPASS\">WP:BYPASS</a> for instructions).</p>";
Morebits.status.root.appendChild(noticebox);
var noticeclear = document.createElement("br");
noticeclear.style.clear = "both";
Morebits.status.root.appendChild(noticeclear);
};
/*
****************************************
*** twinklediff.js: Diff module
****************************************
* Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current")
* Active on: Existing non-special pages
* Config directives in: TwinkleConfig
*/
Twinkle.diff = function twinklediff() {
if( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') ) {
return;
}
var query = {
'title': mw.config.get('wgPageName'),
'diff': 'cur',
'oldid': 'prev'
};
twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Last', 'tw-lastdiff', 'Show most recent diff' );
// Show additional tabs only on diff pages
if (Morebits.queryString.exists('diff')) {
twAddPortletLink(function(){ Twinkle.diff.evaluate(false); }, 'Since', 'tw-since', 'Show difference between last diff and the revision made by previous user' );
twAddPortletLink( function(){ Twinkle.diff.evaluate(true); }, 'Since mine', 'tw-sincemine', 'Show difference between last diff and my last revision' );
var oldid = /oldid=(.+)/.exec($('#mw-diff-ntitle1').find('strong a').first().attr("href"))[1];
query = {
'title': mw.config.get('wgPageName'),
'diff': 'cur',
'oldid' : oldid
};
twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Current', 'tw-curdiff', 'Show difference to current revision' );
}
};
Twinkle.diff.evaluate = function twinklediffEvaluate(me) {
var user;
if( me ) {
user = mw.config.get('wgUserName');
} else {
var node = document.getElementById( 'mw-diff-ntitle2' );
if( ! node ) {
// nothing to do?
return;
}
user = $(node).find('a').first().text();
}
var query = {
'prop': 'revisions',
'action': 'query',
'titles': mw.config.get('wgPageName'),
'rvlimit': 1,
'rvprop': [ 'ids', 'user' ],
'rvstartid': mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one
'rvuser': user
};
Morebits.status.init( document.getElementById('bodyContent') );
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of initial contributor', query, Twinkle.diff.callbacks.main );
wikipedia_api.params = { user: user };
wikipedia_api.post();
};
Twinkle.diff.callbacks = {
main: function( self ) {
var xmlDoc = self.responseXML;
var revid = $(xmlDoc).find('rev').attr('revid');
if( ! revid ) {
self.statelem.error( 'no suitable earlier revision found, or ' + self.params.user + ' is the only contributor. Aborting.' );
return;
}
var query = {
'title': mw.config.get('wgPageName'),
'oldid': revid,
'diff': mw.config.get('wgCurRevisionId')
};
window.location = mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query );
}
};
/*
****************************************
*** twinklefluff.js: Revert/rollback module
****************************************
* Mode of invocation: Links on history, contributions, and diff pages
* Active on: Diff pages, history pages, contributions pages
* Config directives in: TwinkleConfig
*/
/**
Twinklefluff revert and antivandalism utility
*/
Twinkle.fluff = {
auto: function() {
if( parseInt( Morebits.queryString.get('oldid'), 10) !== mw.config.get('wgCurRevisionId') ) {
// not latest revision
alert("Can't rollback, page has changed in the meantime.");
return;
}
var vandal = $("#mw-diff-ntitle2").find("a.mw-userlink").text();
Twinkle.fluff.revert( Morebits.queryString.get( 'twinklerevert' ), vandal, true );
},
normal: function() {
var spanTag = function( color, content ) {
var span = document.createElement( 'span' );
span.style.color = color;
span.appendChild( document.createTextNode( content ) );
return span;
};
if( mw.config.get('wgNamespaceNumber') === -1 && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) {
//Get the username these contributions are for
var lastLogNode = $('#contentSub').find('a[title^="Special:Log"]').last();
if(!lastLogNode) return;
var logMatch = /wiki\/Special:Log\/(.+)$/.exec(
lastLogNode ? lastLogNode.attr("href").replace(/_/g, "%20") : ''
);
if(!logMatch) return;
username = decodeURIComponent(logMatch[1]);
if( Twinkle.getPref('showRollbackLinks').indexOf('contribs') !== -1 ||
( mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').indexOf('others') !== -1 ) ||
( mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').indexOf('mine') !== -1 ) ) {
var list = $("#bodyContent").find("ul li:has(span.mw-uctop)");
var revNode = document.createElement('strong');
var revLink = document.createElement('a');
revLink.appendChild( spanTag( 'Black', '[' ) );
revLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) );
revLink.appendChild( spanTag( 'Black', ']' ) );
revNode.appendChild(revLink);
var revVandNode = document.createElement('strong');
var revVandLink = document.createElement('a');
revVandLink.appendChild( spanTag( 'Black', '[' ) );
revVandLink.appendChild( spanTag( 'Red', 'vandalism' ) );
revVandLink.appendChild( spanTag( 'Black', ']' ) );
revVandNode.appendChild(revVandLink);
list.each(function(key, current) {
var href = $(current).children("a:eq(1)").attr("href");
current.appendChild( document.createTextNode(' ') );
var tmpNode = revNode.cloneNode( true );
tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'norm' } ) );
current.appendChild( tmpNode );
current.appendChild( document.createTextNode(' ') );
tmpNode = revVandNode.cloneNode( true );
tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'vand' } ) );
current.appendChild( tmpNode );
});
}
} else {
if( mw.config.get('wgCanonicalSpecialPageName') === "Undelete" ) {
//You can't rollback deleted pages!
return;
}
var body = document.getElementById('bodyContent');
var firstRev = $("div.firstrevisionheader").length;
if( firstRev ) {
// we have first revision here, nothing to do.
return;
}
var otitle, ntitle;
try {
var otitle1 = document.getElementById('mw-diff-otitle1');
var ntitle1 = document.getElementById('mw-diff-ntitle1');
if (!otitle1 || !ntitle1) {
return;
}
otitle = otitle1.parentNode;
ntitle = ntitle1.parentNode;
} catch( e ) {
// no old, nor new title, nothing to do really, return;
return;
}
var old_rev_url = $("#mw-diff-otitle1").find("strong a").attr("href");
// Lets first add a [edit this revision] link
var query = new Morebits.queryString( old_rev_url.split( '?', 2 )[1] );
var oldrev = query.get('oldid');
var revertToRevision = document.createElement('div');
revertToRevision.setAttribute( 'id', 'tw-revert-to-orevision' );
revertToRevision.style.fontWeight = 'bold';
var revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') );
revertToRevisionLink.href = "#";
$(revertToRevisionLink).click(function(){
Twinkle.fluff.revertToRevision(oldrev);
});
revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) );
revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) );
revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) );
otitle.insertBefore( revertToRevision, otitle.firstChild );
if( document.getElementById('differences-nextlink') ) {
// Not latest revision
curVersion = false;
var new_rev_url = $("#mw-diff-ntitle1").find("strong a").attr("href");
query = new Morebits.queryString( new_rev_url.split( '?', 2 )[1] );
var newrev = query.get('oldid');
revertToRevision = document.createElement('div');
revertToRevision.setAttribute( 'id', 'tw-revert-to-nrevision' );
revertToRevision.style.fontWeight = 'bold';
revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') );
revertToRevisionLink.href = "#";
$(revertToRevisionLink).click(function(){
Twinkle.fluff.revertToRevision(newrev);
});
revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) );
revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) );
revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) );
ntitle.insertBefore( revertToRevision, ntitle.firstChild );
return;
}
if( Twinkle.getPref('showRollbackLinks').indexOf('diff') !== -1 ) {
var vandal = $("#mw-diff-ntitle2").find("a").first().text();
var revertNode = document.createElement('div');
revertNode.setAttribute( 'id', 'tw-revert' );
var agfNode = document.createElement('strong');
var vandNode = document.createElement('strong');
var normNode = document.createElement('strong');
var agfLink = document.createElement('a');
var vandLink = document.createElement('a');
var normLink = document.createElement('a');
agfLink.href = "#";
vandLink.href = "#";
normLink.href = "#";
$(agfLink).click(function(){
Twinkle.fluff.revert('agf', vandal);
});
$(vandLink).click(function(){
Twinkle.fluff.revert('vand', vandal);
});
$(normLink).click(function(){
Twinkle.fluff.revert('norm', vandal);
});
agfLink.appendChild( spanTag( 'Black', '[' ) );
agfLink.appendChild( spanTag( 'DarkOliveGreen', 'rollback (AGF)' ) );
agfLink.appendChild( spanTag( 'Black', ']' ) );
vandLink.appendChild( spanTag( 'Black', '[' ) );
vandLink.appendChild( spanTag( 'Red', 'rollback (VANDAL)' ) );
vandLink.appendChild( spanTag( 'Black', ']' ) );
normLink.appendChild( spanTag( 'Black', '[' ) );
normLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) );
normLink.appendChild( spanTag( 'Black', ']' ) );
agfNode.appendChild(agfLink);
vandNode.appendChild(vandLink);
normNode.appendChild(normLink);
revertNode.appendChild( agfNode );
revertNode.appendChild( document.createTextNode(' || ') );
revertNode.appendChild( normNode );
revertNode.appendChild( document.createTextNode(' || ') );
revertNode.appendChild( vandNode );
ntitle.insertBefore( revertNode, ntitle.firstChild );
}
}
}
};
Twinkle.fluff.revert = function revertPage( type, vandal, autoRevert, rev, page ) {
if (mw.util.isIPv6Address(vandal)) {
vandal = Morebits.sanitizeIPv6(vandal);
}
var pagename = page || mw.config.get('wgPageName');
var revid = rev || mw.config.get('wgCurRevisionId');
Morebits.status.init( document.getElementById('bodyContent') );
var params = {
type: type,
user: vandal,
pagename: pagename,
revid: revid,
autoRevert: !!autoRevert
};
var query = {
'action': 'query',
'prop': ['info', 'revisions'],
'titles': pagename,
'rvlimit': 50, // max possible
'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ],
'curtimestamp': '',
'meta': 'tokens',
'type': 'csrf',
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of earlier revisions', query, Twinkle.fluff.callbacks.main );
wikipedia_api.params = params;
wikipedia_api.post();
};
Twinkle.fluff.revertToRevision = function revertToRevision( oldrev ) {
Morebits.status.init( document.getElementById('bodyContent') );
var query = {
'action': 'query',
'prop': ['info', 'revisions'],
'titles': mw.config.get('wgPageName'),
'rvlimit': 1,
'rvstartid': oldrev,
'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ],
'curtimestamp': '',
'meta': 'tokens',
'type': 'csrf',
'format': 'xml'
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of the earlier revision', query, Twinkle.fluff.callbacks.toRevision.main );
wikipedia_api.params = { rev: oldrev };
wikipedia_api.post();
};
Twinkle.fluff.userIpLink = function( user ) {
return (Morebits.isIPAddress(user) ? "[[Special:Contributions/" : "[[User:" ) + user + "|" + user + "]]";
};
Twinkle.fluff.callbacks = {
toRevision: {
main: function( self ) {
var xmlDoc = self.responseXML;
var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10);
var touched = $(xmlDoc).find('page').attr('touched');
var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp');
var edittoken = $(xmlDoc).find('tokens').attr('csrftoken');
var revertToRevID = $(xmlDoc).find('rev').attr('revid');
var revertToUser = $(xmlDoc).find('rev').attr('user');
if (revertToRevID !== self.params.rev) {
self.statitem.error( 'The retrieved revision does not match the requested revision. Aborting.' );
return;
}
var optional_summary = prompt( "Please specify a reason for the revert: ", "" ); // padded out to widen prompt in Firefox
if (optional_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
var summary = "Reverted to revision " + revertToRevID + " by " + revertToUser + (optional_summary ? ": " + optional_summary : '') + "." +
Twinkle.getPref('summaryAd');
var query = {
'action': 'edit',
'title': mw.config.get('wgPageName'),
'summary': summary,
'token': edittoken,
'undo': lastrevid,
'undoafter': revertToRevID,
'basetimestamp': touched,
'starttimestamp': starttimestamp,
'watchlist': Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined,
'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined
};
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Reversion completed";
var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, null/*Twinkle.fluff.callbacks.toRevision.complete*/, self.statelem);
wikipedia_api.params = self.params;
wikipedia_api.post();
},
complete: function (self) {
}
},
main: function( self ) {
var xmlDoc = self.responseXML;
var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10);
var touched = $(xmlDoc).find('page').attr('touched');
var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp');
var edittoken = $(xmlDoc).find('tokens').attr('csrftoken');
var lastuser = $(xmlDoc).find('rev').attr('user');
var revs = $(xmlDoc).find('rev');
if( revs.length < 1 ) {
self.statelem.error( 'We have less than one additional revision, thus impossible to revert' );
return;
}
var top = revs[0];
if( lastrevid < self.params.revid ) {
Morebits.status.error( 'Error', [ 'The most recent revision ID received from the server, ', Morebits.htmlNode( 'strong', lastrevid ), ', is less than the ID of the displayed revision. This could indicate that the current revision has been deleted, the server is lagging, or that bad data has been received. Will stop proceeding at this point.' ] );
return;
}
var index = 1;
if( self.params.revid !== lastrevid ) {
Morebits.status.warn( 'Warning', [ 'Latest revision ', Morebits.htmlNode( 'strong', lastrevid ), ' doesn\'t equal our revision ', Morebits.htmlNode( 'strong', self.params.revid ) ] );
if( lastuser === self.params.user ) {
switch( self.params.type ) {
case 'vand':
Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume vandalism, we continue to revert' ]);
break;
case 'agf':
Morebits.status.warn( 'Warning', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume good faith, we stop reverting, as the problem might have been fixed.' ]);
return;
default:
Morebits.status.warn( 'Notice', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , ', but we will stop reverting anyway.' ] );
return;
}
}
else if(self.params.type === 'vand' &&
Twinkle.fluff.whiteList.indexOf( top.getAttribute( 'user' ) ) !== -1 && revs.length > 1 &&
revs[1].getAttribute( 'pageId' ) === self.params.revid) {
Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', a trusted bot, and the revision before was made by our vandal, so we proceed with the revert.' ] );
index = 2;
} else {
Morebits.status.error( 'Error', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', so it might have already been reverted, stopping reverting.'] );
return;
}
}
if( Twinkle.fluff.whiteList.indexOf( self.params.user ) !== -1 ) {
switch( self.params.type ) {
case 'vand':
Morebits.status.info( 'Info', [ 'Vandalism revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. As this is a whitelisted bot, we assume you wanted to revert vandalism made by the previous user instead.' ] );
index = 2;
vandal = revs[1].getAttribute( 'user' );
self.params.user = revs[1].getAttribute( 'user' );
break;
case 'agf':
Morebits.status.warn( 'Notice', [ 'Good faith revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, it makes no sense at all to revert it as a good faith edit, will stop reverting.' ] );
return;
case 'norm':
/* falls through */
default:
var cont = confirm( 'Normal revert was chosen, but the most recent edit was made by a whitelisted bot (' + self.params.user + '). Do you want to revert the revision before instead?' );
if( cont ) {
Morebits.status.info( 'Info', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, and per confirmation, we\'ll revert the previous revision instead.' ] );
index = 2;
self.params.user = revs[1].getAttribute( 'user' );
} else {
Morebits.status.warn( 'Notice', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, but per confirmation, revert on top revision will proceed.' ] );
}
break;
}
}
var found = false;
var count = 0;
for( var i = index; i < revs.length; ++i ) {
++count;
if( revs[i].getAttribute( 'user' ) !== self.params.user ) {
found = i;
break;
}
}
if( ! found ) {
self.statelem.error( [ 'No previous revision found. Perhaps ', Morebits.htmlNode( 'strong', self.params.user ), ' is the only contributor, or that the user has made more than ' + Twinkle.getPref('revertMaxRevisions') + ' edits in a row.' ] );
return;
}
if( ! count ) {
Morebits.status.error( 'Error', "We were to revert zero revisions. As that makes no sense, we'll stop reverting this time. It could be that the edit has already been reverted, but the revision ID was still the same." );
return;
}
var good_revision = revs[ found ];
var userHasAlreadyConfirmedAction = false;
if (self.params.type !== 'vand' && count > 1) {
if ( !confirm( self.params.user + ' has made ' + count + ' edits in a row. Are you sure you want to revert them all?') ) {
Morebits.status.info( 'Notice', 'Stopping reverting per user input' );
return;
}
userHasAlreadyConfirmedAction = true;
}
self.params.count = count;
self.params.goodid = good_revision.getAttribute( 'revid' );
self.params.gooduser = good_revision.getAttribute( 'user' );
self.statelem.status( [ ' revision ', Morebits.htmlNode( 'strong', self.params.goodid ), ' that was made ', Morebits.htmlNode( 'strong', count ), ' revisions ago by ', Morebits.htmlNode( 'strong', self.params.gooduser ) ] );
var summary, extra_summary, userstr, gooduserstr;
switch( self.params.type ) {
case 'agf':
extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox
if (extra_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
userHasAlreadyConfirmedAction = true;
userstr = self.params.user;
summary = "Reverted good faith edits by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" +
userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd');
break;
case 'vand':
userstr = self.params.user;
gooduserstr = self.params.gooduser;
summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" +
userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]]) identified as vandalism to last revision by " +
gooduserstr + "." + Twinkle.getPref('summaryAd');
break;
case 'norm':
/* falls through */
default:
if( Twinkle.getPref('offerReasonOnNormalRevert') ) {
extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox
if (extra_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
userHasAlreadyConfirmedAction = true;
}
userstr = self.params.user;
summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" +
userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) +
Twinkle.getPref('summaryAd');
break;
}
if (Twinkle.getPref('confirmOnFluff') && !userHasAlreadyConfirmedAction && !confirm("Reverting page: are you sure?")) {
self.statelem.error( 'Aborted by user.' );
return;
}
var query;
if( (!self.params.autoRevert || Twinkle.getPref('openTalkPageOnAutoRevert')) &&
Twinkle.getPref('openTalkPage').indexOf( self.params.type ) !== -1 &&
mw.config.get('wgUserName') !== self.params.user ) {
Morebits.status.info( 'Info', [ 'Opening user talk page edit form for user ', Morebits.htmlNode( 'strong', self.params.user ) ] );
query = {
'title': 'User talk:' + self.params.user,
'action': 'edit',
'preview': 'yes',
'vanarticle': self.params.pagename.replace(/_/g, ' '),
'vanarticlerevid': self.params.revid,
'vanarticlegoodrevid': self.params.goodid,
'type': self.params.type,
'count': self.params.count
};
switch( Twinkle.getPref('userTalkPageMode') ) {
case 'tab':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' );
break;
case 'blank':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
case 'window':
/* falls through */
default:
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
}
}
query = {
'action': 'edit',
'title': self.params.pagename,
'summary': summary,
'token': edittoken,
'undo': lastrevid,
'undoafter': self.params.goodid,
'basetimestamp': touched,
'starttimestamp': starttimestamp,
'watchlist' : Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined,
'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined
};
Morebits.wiki.actionCompleted.redirect = self.params.pagename;
Morebits.wiki.actionCompleted.notice = "Reversion completed";
var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, Twinkle.fluff.callbacks.complete, self.statelem);
wikipedia_api.params = self.params;
wikipedia_api.post();
},
complete: function (self) {
self.statelem.info("done");
}
};
Twinkle.fluff.formatSummaryPostfix = function(stringToAdd) {
if (stringToAdd) {
stringToAdd = ': ' + Morebits.string.toUpperCaseFirstChar(stringToAdd);
if (stringToAdd.search(/[.?!;]$/) === -1) {
stringToAdd = stringToAdd + '.';
}
return stringToAdd;
}
else {
return '.';
}
};
Twinkle.fluff.init = function twinklefluffinit() {
if (twinkleUserAuthorized)
{
// a list of usernames, usually only bots, that vandalism revert is jumped over, that is
// if vandalism revert was chosen on such username, then it's target is on the revision before.
// This is for handeling quick bots that makes edits seconds after the original edit is made.
// This only affect vandalism rollback, for good faith rollback, it will stop, indicating a bot
// has no faith, and for normal rollback, it will rollback that edit.
Twinkle.fluff.whiteList = [
'AnomieBOT',
'ClueBot NG',
'SineBot'
];
if ( Morebits.queryString.exists( 'twinklerevert' ) ) {
Twinkle.fluff.auto();
} else {
Twinkle.fluff.normal();
}
}
};
/*
****************************************
*** twinklespeedy.js: CSD module
****************************************
* Mode of invocation: Tab ("CSD")
* Active on: Non-special, existing pages
* Config directives in: TwinkleConfig
*
* NOTE FOR DEVELOPERS:
* If adding a new criterion, check out the default values of the CSD preferences
* in twinkle.header.js, and add your new criterion to those if you think it would
* be good.
*/
Twinkle.speedy = function twinklespeedy() {
// Disable on:
// * special pages
// * non-existent pages
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
twAddPortletLink( Twinkle.speedy.callback, "Del", "tw-csd", Morebits.userIsInGroup('sysop') ? "Delete page" : "Request deletion" );
};
// This function is run when the CSD tab/header link is clicked
Twinkle.speedy.callback = function twinklespeedyCallback() {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
Twinkle.speedy.initDialog(Morebits.userIsInGroup( 'sysop' ) ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true);
};
Twinkle.speedy.dialog = null; // used by unlink feature
// Prepares the speedy deletion dialog and displays it
Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) {
var dialog;
Twinkle.speedy.dialog = new Morebits.simpleWindow( Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight') );
dialog = Twinkle.speedy.dialog;
dialog.setTitle( "Choose criteria for deletion" );
dialog.setScriptName( "Twinkle" );
//dialog.addFooterLink( "Quick deletion policy", "Wikipedia:Deletion policy#Quick deletion" );
dialog.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#speedy" );
var form = new Morebits.quickForm( callbackfunc, (Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null) );
if( Morebits.userIsInGroup( 'sysop' ) ) {
form.append( {
type: 'checkbox',
list: [
{
label: 'Tag page only, don\'t delete',
value: 'tag_only',
name: 'tag_only',
tooltip: 'If you just want to tag the page, instead of deleting it now',
checked : Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
var cForm = event.target.form;
var cChecked = event.target.checked;
// enable/disable talk page checkbox
if (cForm.talkpage) {
cForm.talkpage.disabled = cChecked;
cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete');
}
// enable/disable redirects checkbox
cForm.redirects.disabled = cChecked;
cForm.redirects.checked = !cChecked;
// enable/disable notify checkbox
cForm.notify.disabled = !cChecked;
cForm.notify.checked = cChecked;
// enable/disable multiple
cForm.multiple.disabled = !cChecked;
cForm.multiple.checked = false;
Twinkle.speedy.callback.dbMultipleChanged(cForm, false);
event.stopPropagation();
}
}
]
} );
form.append( { type: 'header', label: 'Delete-related options' } );
if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page
form.append( {
type: 'checkbox',
list: [
{
label: 'Also delete talk page',
value: 'talkpage',
name: 'talkpage',
tooltip: "This option deletes the page's talk page in addition. If you choose the F8 (moved to Commons) criterion, this option is ignored and the talk page is *not* deleted.",
checked: Twinkle.getPref('deleteTalkPageOnDelete'),
disabled: Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
}
form.append( {
type: 'checkbox',
list: [
{
label: 'Also delete all redirects',
value: 'redirects',
name: 'redirects',
tooltip: "This option deletes all incoming redirects in addition. Avoid this option for procedural (e.g. move/merge) deletions.",
checked: true,
disabled: Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
form.append( { type: 'header', label: 'Tag-related options' } );
}
form.append( {
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notify',
tooltip: "A notification template will be placed on the talk page of the creator, IF you have a notification enabled in your Twinkle preferences " +
"for the criterion you choose AND this box is checked. The creator may be welcomed as well.",
checked: !Morebits.userIsInGroup( 'sysop' ) || Twinkle.getPref('deleteSysopDefaultToTag'),
disabled: Morebits.userIsInGroup( 'sysop' ) && !Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
form.append( {
type: 'div',
name: 'work_area',
label: 'Failed to initialize the CSD module. Please try again, or tell the Twinkle developers about the issue.'
} );
if( Twinkle.getPref( 'speedySelectionStyle' ) !== 'radioClick' ) {
form.append( { type: 'submit' } );
}
var result = form.render();
dialog.setContent( result );
dialog.display();
Twinkle.speedy.callback.dbMultipleChanged( result, false );
};
Twinkle.speedy.callback.dbMultipleChanged = function twinklespeedyCallbackDbMultipleChanged(form, checked) {
var namespace = mw.config.get('wgNamespaceNumber');
var value = checked;
var work_area = new Morebits.quickForm.element( {
type: 'div',
name: 'work_area'
} );
if (checked && Twinkle.getPref('speedySelectionStyle') === 'radioClick') {
work_area.append( {
type: 'div',
label: 'When finished choosing criteria, click:'
} );
work_area.append( {
type: 'button',
name: 'submit-multiple',
label: 'Submit Query',
event: function( event ) {
Twinkle.speedy.callback.evaluateUser( event );
event.stopPropagation();
}
} );
}
var radioOrCheckbox = (value ? 'checkbox' : 'radio');
/*
if (namespace % 2 === 1 && namespace !== 3) { // talk pages, but not user talk pages
work_area.append( { type: 'header', label: 'Talk pages' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.talkList } );
}
switch (namespace) {
case 0: // article
case 1: // talk
work_area.append( { type: 'header', label: 'Articles' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getArticleList(value) } );
break;
case 2: // user
case 3: // user talk
work_area.append( { type: 'header', label: 'User pages' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.userList } );
break;
case 6: // file
case 7: // file talk
work_area.append( { type: 'header', label: 'Files' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getFileList(value) } );
break;
case 10: // template
case 11: // template talk
work_area.append( { type: 'header', label: 'Templates' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.templateList } );
break;
case 14: // category
case 15: // category talk
work_area.append( { type: 'header', label: 'Categories' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.categoryList } );
break;
default:
break;
}
*/
work_area.append( { type: 'header', label: 'General criteria' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getGeneralList(value) });
/*
work_area.append( { type: 'header', label: 'Redirects' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.redirectList } );
*/
var old_area = Morebits.quickForm.getElements(form, "work_area")[0];
form.replaceChild(work_area.render(), old_area);
};
Twinkle.speedy.talkList = [
/*{
label: 'G8: Talk pages with no page belonging to it',
value: 'talk',
tooltip: 'This does not include any page that is useful to the project - for example user talk pages, talk page archives, and talk pages for files that exist on Wikimedia Commons.'
}*/
];
// this is a function to allow for db-multiple filtering
Twinkle.speedy.getFileList = function twinklespeedyGetFileList(multiple) {
var result = [];
/*result.push({
label: 'F1: Not allowed',
value: 'prohibitedimage',
tooltip: 'Most media uploads are not allowed on Simple English Wikipedia. They should be uploaded to Wikimedia Commons instead. There are a few exceptions to this rule. Firstly, all spoken articles should be uploaded here, as they are for local use. Secondly, there are some logos that Commons does not accept, but are needed here, for example Image:Wiki.png, which is used as the Wikipedia logo.'
});*/
return result;
};
Twinkle.speedy.getArticleList = function twinklespeedyGetArticleList(multiple) {
var result = [];
/*result.push({
label: 'A1: Little or no meaning',
value: 'nocontext',
tooltip: 'Is very short and providing little or no meaning (e.g., "He is a funny man that has created Factory and the Hacienda. And, by the way, his wife is great."). Having a small amount of content is not a reason to delete if it has useful information.'
});
result.push({
label: 'A2: No content',
value: 'nocontent',
tooltip: 'Has no content. This includes any article consisting only of links (including hyperlinks, category tags and "see also" sections), a rephrasing of the title, and/or attempts to correspond with the person or group named by its title. This does not include disambiguation pages.'
});
result.push({
label: 'A3: Article that exists on another Wikimedia project',
value: 'transwiki',
tooltip: 'Has been copied and pasted from another Wikipedia: Any article or section from an article that has been copied and pasted with little or no change.'
});
result.push({
label: 'A4: People, groups, companies, products, services or websites that do not claim to be notable.',
value: 'notability',
tooltip: 'An article about a real person, group of people, band, club, company, product, service or or web content that does not say why it is important. If not everyone agrees that the subject is not notable or there has been a previous RfD, the article may not be quickly deleted, and should be discussed at RfD instead.'
});
result.push({
label: 'A5: Not written in English',
value: 'foreign',
tooltip: 'Any article that is not written in English. An article that is written in any other languages but English.'
});
result.push({
label: 'A6: Obvious hoax',
value: 'hoax',
tooltip: 'Is an obvious hoax. An article that is surely fake or impossible.'
});*/
return result;
};
Twinkle.speedy.categoryList = [
/*{
label: 'C1: Empty categories',
value: 'catempty',
tooltip: '(with no articles or subcategories for at least four days) whose only content includes links to parent categories. However, this can not be used on categories still being discussed on WP:RfD, or disambiguation categories. If the category wasn\'t newly made, it is possible that it used to have articles, and more inspection is needed.'
},
{
label: 'C2: Quick renaming',
value: 'catqr',
tooltip: 'Empty categories that have already been renamed.'
},
{
label: 'C3: Template categories',
value: 'catfd',
tooltip: 'If a category contains articles from only one template (such as Category:Cleanup needed from \{\{cleanup\}\}) and the template is deleted after being discussed, the category can also be deleted without being discussed.'
}*/
];
Twinkle.speedy.userList = [
/*{
label: 'U1: User request',
value: 'userreq',
tooltip: 'User pages can be deleted if its user wants to, but there are some exceptions.'
},
{
label: 'U2: Nonexistent user',
value: 'nouser',
tooltip: 'User pages of users that do not exist. Administrators should check Special:Contributions and Special:DeletedContributions.'
}*/
];
Twinkle.speedy.templateList = [
/*{
label: 'T2: They are deprecated or replaced by a newer template and are completely unused and not linked to.',
value: 'replaced',
tooltip: 'For any template that should not be deleted quickly, use Wikipedia:Requests for deletion.'
}*/
//});
// return result;
];
Twinkle.speedy.getGeneralList = function twinklespeedyGetGeneralList(multiple) {
var result = [];
if (!multiple) {
result.push({
label: 'Custom rationale' + (Morebits.userIsInGroup('sysop') ? ' (custom deletion reason)' : ' using {'+'{delete|reason}}'),
value: 'reason',
tooltip: 'You can enter an custom reason.'
});
}
/*result.push({
label: 'G1: Nonsense',
value: 'nonsense',
tooltip: 'All of the text is nonsense. Nonsense includes content that does not make sense or is not meaningful. However, this does not include bad writing, bad words, vandalism, things that are fake or impossible, or parts which are not in English. '
});
result.push({
label: 'G2: Test page',
value: 'test',
tooltip: 'It is a test page, such as "Can I really create a page here?".'
});
result.push({
label: 'G3: Complete vandalism',
value: 'vandalism',
tooltip: 'The content is completely vandalism.'
});
result.push({
label: 'G4: Recreation of deleted material already deleted at RfD',
value: 'repost',
tooltip: 'Creation of content that is already deleted. It includes an identical or similar copy, with any title, of a page that was deleted, after being discussed in Requests for deletion, unless it was undeleted due to another discussion or was recreated in the user space. Before deleting again, the Administrator should be sure that the content is similar and not just a new article on the same subject. This rule cannot be used if the content had already been quickly deleted before.'
});
if (!multiple) {
result.push({
label: 'G6: History merge',
value: 'histmerge',
tooltip: 'Temporarily deleting a page in order to merge page histories'
});
result.push({
label: 'G6: Move',
value: 'move',
tooltip: 'Making way for a noncontroversial move like reversing a redirect'
});
result.push({
label: 'G6: RfD',
value: 'afd',
tooltip: 'An admin has closed a RfD as "delete".'
});
}
result.push({
label: 'G6: Housekeeping',
value: 'g6',
tooltip: 'Other non-controversial "housekeeping" tasks'
});
result.push({
label: 'G7: Author requests deletion, or author blanked',
value: 'author',
tooltip: 'Any page whose original author wants deletion, can be quickly deleted, but only if most of the page was written by that author and was created as a mistake. If the author blanks the page, this can mean that he or she wants it deleted.'
});
result.push({
label: 'G8: Pages dependent on a non-existent or deleted page',
value: 'talk',
tooltip: '... can be deleted, unless they contain discussion on deletion that can\'t be found anywhere else. Subpages of a talk page can only be deleted under this rule if their top-level page does not exist. This also applies to broken redirects. However, this cannot be used on user talk pages or talk pages of images on Commons.'
});
if (!multiple) {
result.push({
label: 'G8: Subpages with no parent page',
value: 'subpage',
tooltip: 'This excludes any page that is useful to the project, and in particular: deletion discussions that are not logged elsewhere, user and user talk pages, talk page archives, plausible redirects that can be changed to valid targets, and file pages or talk pages for files that exist on Wikimedia Commons.'
});
}
result.push({
label: 'G10: Attack page',
value: 'attack',
tooltip: 'Pages that were only created to insult a person or thing (such as "John Q. Doe is dumb"). This includes articles on a living person that is insult and without sources, where there is no NPOV version in the edit history to revert to.'
});
result.push({
label: 'G11: Obvious advertising',
value: 'spam',
tooltip: 'Pages which were created only to say good things about a company, item, group or service and which would need to be written again so that they can sound like an encyclopedia. However, simply having a company, item, group or service as its subject does not mean that an article can be deleted because of this rule: an article that is obvious advertising should have content that shouldn\'t be in an encyclopedia. If a page has already gone through RfD or QD and was not deleted, it should not be quickly deleted using this rule.'
});
result.push({
label: 'G12: Obviously breaking copyright law',
value: 'copyvio',
tooltip: 'Obviously breaking copyright law like a page which is 1) Copied from another website which does not have a license that can be used with Wikipedia; 2) Containing no content in the page history that is worth being saved. 3) Made by one person instead of being created on wiki and then copied by another website such as one of the many Wikipedia mirror websites. 4) Added by someone who doesn\'t tell if he got permission to do so or not, or if his claim has a large chance of not being true;'
});*/
return result;
};
Twinkle.speedy.redirectList = [
/*{
label: 'R1: Redirects to a non-existent page.',
value: 'redirnone',
tooltip: 'Redirects to a non-existent page.'
},
{
label: 'R2: Redirects from mainspace to any other namespace except the Category:, Template:, Wikipedia:, Help: and Portal: namespaces',
value: 'rediruser',
tooltip: '(this does not include the Wikipedia shortcut pseudo-namespaces). If this was the result of a page move, consider waiting a day or two before deleting the redirect'
},
{
label: 'R3: Redirects as a result of an implausible typo that were recently created',
value: 'redirtypo',
tooltip: 'However, redirects from common misspellings or misnomers are generally useful, as are redirects in other languages'
}*/
];
Twinkle.speedy.normalizeHash = {
'reason': 'db',
'nonsense': 'g1',
'test': 'g2',
'vandalism': 'g3',
'hoax': 'g3',
'repost': 'g4',
'histmerge': 'g6',
'move': 'g6',
'afd': 'g6',
'g6': 'g6',
'author': 'g7',
'talk': 'g8',
'subpage': 'g8',
'attack': 'g10',
'spam': 'g11',
'copyvio': 'g12',
'nocontext': 'a1',
'nocontent': 'a2',
'transwiki': 'a3',
'notability': 'a4',
'foreign': 'a5',
'hoax': 'a6',
'redirnone': 'r1',
'rediruser': 'r2',
'redirtypo': 'r3',
'prohibitedimage': 'f1',
'catempty': 'c1',
'catqr': 'c2',
'catfd': 'c3',
'userreq': 'u1',
'nouser': 'u2',
'replaced':'t2'
};
// keep this synched with [[MediaWiki:Deletereason-dropdown]]
Twinkle.speedy.reasonHash = {
'reason': '',
'nonsense': 'was all nonsense',
'test': 'was a test page',
'vandalism': 'was vandalism',
'pagemove': 'was a redirect created during cleanup of page move vandalism',
'repost': 'was a copy of a page that was deleted by RfD',
'histmerge': 'was in the way of trying to fix or clean up something',
'move': 'was in the way of making a move',
'afd': 'was closed as delete in a RfD',
'g6': 'was housekeeping',
'author': 'was asked to be deleted by the author',
'blanked': 'was implied to be deleted by the author',
'talk': 'was a talk page of a page that does not exist',
'attack': 'was an attack page',
'spam': 'was advertising',
'copyvio': 'was breaking copyright law',
'nocontext': 'was a page that had little or no meaning',
'nocontent': 'was a page that had no content',
'transwiki': 'was copied from another Wikipedia',
'notability': 'was a page that didn\'t say why the subject was notable',
'foreign': 'was not written in English',
'hoax': 'was obviously a hoax (not true)',
'redirnone': 'was a redirect to a page that does not exist',
'rediruser': 'was a redirect to the Talk:, User: or User talk: space',
'redirtypo': 'was a redirect with an uncommon typo',
'prohibitedimage': 'was an image/media that is not allowed on Wikipedia',
'catempty': 'was an empty category',
'catqr': 'was a renamed category',
'catfd': 'was a category containing articles from a now deleted template',
'userreq': 'was a user page whose user requested deletion',
'nouser': 'was a user page of a user that did not exist',
'replaced': 'was deprecated or replaced by a newer template and are completely unused and not linked to'
};
Twinkle.speedy.callbacks = {
sysop: {
main: function( params ) {
var thispage = new Morebits.wiki.page( mw.config.get('wgPageName'), "Deleting page" );
// delete page
var reason;
if (params.normalized === 'db') {
reason = prompt("Enter the deletion summary to use, which will be entered into the deletion log:", "");
} else {
var presetReason = "[[WP:QD#" + params.normalized.toUpperCase() + "|" + params.normalized.toUpperCase() + "]]: " + params.reason;
if (Twinkle.getPref("promptForSpeedyDeletionSummary").indexOf(params.normalized) !== -1) {
reason = prompt("Enter the deletion summary to use, or press OK to accept the automatically generated one.", presetReason);
} else {
reason = presetReason;
}
}
if (!reason || !reason.replace(/^\s*/, "").replace(/\s*$/, "")) {
Morebits.status.error("Asking for reason", "you didn't give one. I don't know... what with admins and their apathetic antics... I give up...");
return;
}
thispage.setEditSummary( reason + Twinkle.getPref('deletionSummaryAd') );
thispage.deletePage();
// delete talk page
if (params.deleteTalkPage &&
params.normalized !== 'f8' &&
document.getElementById( 'ca-talk' ).className !== 'new') {
var talkpage = new Morebits.wiki.page( Morebits.wikipedia.namespaces[ mw.config.get('wgNamespaceNumber') + 1 ] + ':' + mw.config.get('wgTitle'), "Deleting talk page" );
talkpage.setEditSummary('Talk page of deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd'));
talkpage.deletePage();
}
// promote Unlink tool
var $link, $bigtext;
if( mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f8' ) {
$link = $('<a/>', {
'href': '#',
'text': 'click here to go to the Unlink tool',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' },
'click': function(){
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback("Removing usages of and/or links to deleted file " + mw.config.get('wgPageName'));
}
});
$bigtext = $('<span/>', {
'text': 'To orphan backlinks and remove instances of file usage',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' }
});
Morebits.status.info($bigtext[0], $link[0]);
} else if (params.normalized !== 'f8') {
$link = $('<a/>', {
'href': '#',
'text': 'click here to go to the Unlink tool',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' },
'click': function(){
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback("Removing links to deleted page " + mw.config.get('wgPageName'));
}
});
$bigtext = $('<span/>', {
'text': 'To orphan backlinks',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' }
});
Morebits.status.info($bigtext[0], $link[0]);
}
// open talk page of first contributor
if( params.openusertalk ) {
thispage = new Morebits.wiki.page( mw.config.get('wgPageName') ); // a necessary evil, in order to clear incorrect status text
thispage.setCallbackParameters( params );
thispage.lookupCreator( Twinkle.speedy.callbacks.sysop.openUserTalkPage );
}
// delete redirects
if (params.deleteRedirects) {
var query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'redirects',
'bltitle': mw.config.get('wgPageName'),
'bllimit': 5000 // 500 is max for normal users, 5000 for bots and sysops
};
var wikipedia_api = new Morebits.wiki.api( 'getting list of redirects...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain,
new Morebits.status( 'Deleting redirects' ) );
wikipedia_api.params = params;
wikipedia_api.post();
}
},
openUserTalkPage: function( pageobj ) {
pageobj.getStatusElement().unlink(); // don't need it anymore
var user = pageobj.getCreator();
var statusIndicator = new Morebits.status('Opening user talk page edit form for ' + user, 'opening...');
var query = {
'title': 'User talk:' + user,
'action': 'edit',
'preview': 'yes',
'vanarticle': mw.config.get('wgPageName').replace(/_/g, ' ')
};
switch( Twinkle.getPref('userTalkPageMode') ) {
case 'tab':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' );
break;
case 'blank':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
case 'window':
/* falls through */
default :
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
}
statusIndicator.info( 'complete' );
},
deleteRedirectsMain: function( apiobj ) {
var xmlDoc = apiobj.getXML();
var $snapshot = $(xmlDoc).find('backlinks bl');
var total = $snapshot.length;
if( !total ) {
return;
}
var statusIndicator = apiobj.statelem;
statusIndicator.status("0%");
var onsuccess = function( apiobj ) {
var obj = apiobj.params.obj;
var total = apiobj.params.total;
var now = parseInt( 100 * ++(apiobj.params.current)/total, 10 ) + '%';
obj.update( now );
apiobj.statelem.unlink();
if( apiobj.params.current >= total ) {
obj.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
var params = $.extend( {}, apiobj.params );
params.current = 0;
params.total = total;
params.obj = statusIndicator;
$snapshot.each(function(key, value) {
var title = $(value).attr('title');
var page = new Morebits.wiki.page(title, 'Deleting redirect "' + title + '"');
page.setEditSummary('Redirect to deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd'));
page.deletePage(onsuccess);
});
}
},
user: {
main: function(pageobj) {
var statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error( "It seems that the page doesn't exist; perhaps it has already been deleted" );
return;
}
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
statelem.status( 'Checking for tags on the page...' );
// check for existing deletion tags
var tag = /(?:\{\{\s*(qd|qd-multiple|db|delete|db-.*?)(?:\s*\||\s*\}\}))/.exec( text );
if( tag ) {
statelem.error( [ Morebits.htmlNode( 'strong', tag[1] ) , " is already placed on the page." ] );
return;
}
var xfd = /(?:\{\{([rsaiftcm]fd|md1|proposed deletion)[^{}]*?\}\})/i.exec( text );
if( xfd && !confirm( "The deletion-related template {{" + xfd[1] + "}} was found on the page. Do you still want to add a CSD template?" ) ) {
return;
}
var code, parameters, i;
if (params.normalizeds.length > 1)
{
code = "{{QD-multiple";
var breakFlag = false;
$.each(params.normalizeds, function(index, norm) {
code += "|" + norm.toUpperCase();
parameters = Twinkle.speedy.getParameters(params.values[index], norm, statelem);
if (!parameters) {
breakFlag = true;
return false; // the user aborted
}
for (i in parameters) {
if (typeof parameters[i] === 'string' && !parseInt(i, 10)) { // skip numeric parameters - {{db-multiple}} doesn't understand them
code += "|" + i + "=" + parameters[i];
}
}
});
if (breakFlag) {
return;
}
code += "}}";
params.utparams = [];
}
else
{
parameters = Twinkle.speedy.getParameters(params.values[0], params.normalizeds[0], statelem);
if (!parameters) {
return; // the user aborted
}
code = "{{delete|" + params.normalizeds;
for (i in parameters) {
if (typeof parameters[i] === 'string') {
code += "|" + i + "=" + parameters[i];
}
}
code += "|editor=" + mw.config.get("wgUserName") + "|date=~~~~~";
code += "}}";
params.utparams = Twinkle.speedy.getUserTalkParameters(params.normalizeds[0], parameters);
}
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
// patrol the page, if reached from Special:NewPages
if( Twinkle.getPref('markSpeedyPagesAsPatrolled') ) {
thispage.patrol();
}
// Wrap SD template in noinclude tags if we are in template space.
// Won't work with userboxes in userspace, or any other transcluded page outside template space
if (mw.config.get('wgNamespaceNumber') === 10) { // Template:
code = "<noinclude>" + code + "</noinclude>";
}
// Remove tags that become superfluous with this action
if (mw.config.get('wgNamespaceNumber') === 6) {
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = text.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)[^}]*\}\}/gi, "");
}
// Generate edit summary for edit
var editsummary;
if (params.normalizeds.length > 1) {
editsummary = 'Requesting quick deletion (';
$.each(params.normalizeds, function(index, norm) {
editsummary += '[[WP:QD#' + norm.toUpperCase() + '|QD ' + norm.toUpperCase() + ']], ';
});
editsummary = editsummary.substr(0, editsummary.length - 2); // remove trailing comma
editsummary += ').';
} else if (params.normalizeds[0] === "db") {
editsummary = 'Requesting deletion with criteria \"' + parameters["1"] + '\".';
} else if (params.values[0] === "histmerge") {
editsummary = "Requesting history merge with [[" + parameters["1"] + "]]";
} else {
editsummary = "Requesting deletion (" + params.normalizeds[0].toUpperCase() + ")";
}
pageobj.setPageText(code + ((params.normalizeds.indexOf('g10') !== -1) ? '' : ("\n" + text) )); // cause attack pages to be blanked
pageobj.setEditSummary(editsummary + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(params.watch);
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.speedy.callbacks.user.tagComplete);
},
tagComplete: function(pageobj) {
var params = pageobj.getCallbackParameters();
// Notification to first contributor
if (params.usertalk) {
var callback = function(pageobj) {
var initialContrib = pageobj.getCreator();
// don't notify users when their user talk page is nominated
if (initialContrib === mw.config.get('wgTitle') && mw.config.get('wgNamespaceNumber') === 3) {
Morebits.status.warn("Notifying initial contributor: this user created their own user talk page; skipping notification");
return;
}
var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"),
notifytext, i;
// specialcase "db" and "db-multiple"
if (params.normalizeds.length > 1) {
notifytext = "\n{{subst:QD-notice-multiple|page=" + mw.config.get('wgPageName');
var count = 2;
$.each(params.normalizeds, function(index, norm) {
notifytext += "|" + (count++) + "=" + norm.toUpperCase();
});
} else if (params.normalizeds[0] === "db") {
notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds;
} else {
notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds;
}
for (i in params.utparams) {
if (typeof params.utparams[i] === 'string') {
notifytext += "|" + i + "=" + params.utparams[i];
}
}
notifytext += (params.welcomeuser ? "" : "|nowelcome=yes") + "}} ~~~~";
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary("Notification: quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
usertalkpage.setCreateOption('recreate');
usertalkpage.setFollowRedirect(true);
usertalkpage.append();
// add this nomination to the user's userspace log, if the user has enabled it
if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params, initialContrib);
}
};
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
thispage.lookupCreator(callback);
}
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
else if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params, null);
}
},
// note: this code is also invoked from twinkleimage
// the params used are:
// for CSD: params.values, params.normalizeds (note: normalizeds is an array)
// for DI: params.fromDI = true, params.type, params.normalized (note: normalized is a string)
addToLog: function(params, initialContrib) {
var wikipedia_page = new Morebits.wiki.page("User:" + mw.config.get('wgUserName') + "/" + Twinkle.getPref('speedyLogPageName'), "Adding entry to userspace log");
params.logInitialContrib = initialContrib;
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.saveLog);
},
saveLog: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
// add blurb if log page doesn't exist
if (!pageobj.exists()) {
text =
"This is a log of all deletion nominations made by this user using [[mh:dev:Twinkle|Twinkle]]'s QD module.\n\n" +
"If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and " +
"request deletion for this page.\n";
if (Morebits.userIsInGroup("sysop")) {
text += "\nThis log does not track outright speedy deletions made using Twinkle.\n";
}
}
// create monthly header
var date = new Date();
var headerRe = new RegExp("^==+\\s*" + date.getUTCMonthName() + "\\s+" + date.getUTCFullYear() + "\\s*==+", "m");
if (!headerRe.exec(text)) {
text += "\n\n=== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ===";
}
text += "\n# [[:" + mw.config.get('wgPageName') + "]]: ";
if (params.fromDI) {
text += "DI [[WP:QD#" + params.normalized.toUpperCase() + "|QD " + params.normalized.toUpperCase() + "]] (" + params.type + ")";
} else {
if (params.normalizeds.length > 1) {
text += "multiple criteria (";
$.each(params.normalizeds, function(index, norm) {
text += "[[WP:QD#" + norm.toUpperCase() + "|" + norm.toUpperCase() + ']], ';
});
text = text.substr(0, text.length - 2); // remove trailing comma
text += ')';
} else if (params.normalizeds[0] === "db") {
text += "{{tl|QD}}";
} else {
text += "[[WP:QD#" + params.normalizeds[0].toUpperCase() + "|CSD " + params.normalizeds[0].toUpperCase() + "]] ({{tl|db-" + params.values[0] + "}})";
}
}
if (params.logInitialContrib) {
text += "; notified {{user|" + params.logInitialContrib + "}}";
}
text += " ~~~~~\n";
pageobj.setPageText(text);
pageobj.setEditSummary("Logging quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption("recreate");
pageobj.save();
}
}
};
// prompts user for parameters to be passed into the speedy deletion tag
Twinkle.speedy.getParameters = function twinklespeedyGetParameters(value, normalized, statelem)
{
var parameters = [];
switch( normalized ) {
case 'db':
var dbrationale = prompt('Please enter a custom reason. \n\"This page can be quickly deleted because:\"', "");
if (!dbrationale || !dbrationale.replace(/^\s*/, "").replace(/\s*$/, ""))
{
statelem.error( 'You must specify a reason. Aborted by user.' );
return null;
}
parameters["1"] = dbrationale;
break;
case 'g12':
var url = prompt( '[QD G12] Please enter the URL if available, including the "http://":', "" );
if (url === null)
{
statelem.error( 'Aborted by user.' );
return null;
}
parameters.url = url;
break;
default:
var defaultreason = prompt('You can enter more details here. \n' +
"Just click OK if you don't want or need to.", "");
if (defaultreason === null) {
return true; // continue to next tag
} else if (defaultreason !== "") {
parameters["2"] = defaultreason;
}
break;
}
return parameters;
};
// function for processing talk page notification template parameters
Twinkle.speedy.getUserTalkParameters = function twinklespeedyGetUserTalkParameters(normalized, parameters)
{
var utparams = [];
switch (normalized)
{
case 'db':
utparams["2"] = parameters["1"];
break;
case 'a10':
utparams.key1 = "article";
utparams.value1 = parameters.article;
break;
default:
break;
}
return utparams;
};
Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) {
var values = (e.target.form ? e.target.form : e.target).getChecked('csd');
if (values.length === 0) {
alert( "Please select a criterion!" );
return null;
}
return values;
};
Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e)
{
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var form = (e.target.form ? e.target.form : e.target);
var tag_only = form.tag_only;
if( tag_only && tag_only.checked ) {
Twinkle.speedy.callback.evaluateUser(e);
return;
}
var value = Twinkle.speedy.resolveCsdValues(e)[0];
if (!value) {
return;
}
var normalized = Twinkle.speedy.normalizeHash[ value ];
var params = {
value: value,
normalized: normalized,
watch: Twinkle.getPref('watchSpeedyPages').indexOf( normalized ) !== -1,
reason: Twinkle.speedy.reasonHash[ value ],
openusertalk: Twinkle.getPref('openUserTalkPageOnSpeedyDelete').indexOf( normalized ) !== -1,
deleteTalkPage: form.talkpage && form.talkpage.checked,
deleteRedirects: form.redirects.checked
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Twinkle.speedy.callbacks.sysop.main( params );
};
Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var form = (e.target.form ? e.target.form : e.target);
if (e.target.type === "checkbox") {
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
//var multiple = form.multiple.checked;
var normalizeds = [];
$.each(values, function(index, value) {
var norm = Twinkle.speedy.normalizeHash[ value ];
// for sysops only
if (['f4', 'f5', 'f6', 'f11'].indexOf(norm) !== -1) {
alert("Tagging with F4, F5, F6, and F11 is not possible using the CSD module. Try using DI instead, or unchecking \"Tag page only\" if you meant to delete the page.");
return;
}
normalizeds.push(norm);
});
// analyse each criterion to determine whether to watch the page/notify the creator
var watchPage = false;
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('watchSpeedyPages').indexOf(norm) !== -1) {
watchPage = true;
return false; // break
}
});
var notifyuser = false;
if (form.notify.checked) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('notifyUserOnSpeedyDeletionNomination').indexOf(norm) !== -1) {
notifyuser = true;
return false; // break
}
});
}
var welcomeuser = false;
if (notifyuser) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('welcomeUserOnSpeedyDeletionNotification').indexOf(norm) !== -1) {
welcomeuser = true;
return false; // break
}
});
}
var csdlog = false;
if (Twinkle.getPref('logSpeedyNominations')) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('noLogOnSpeedyNomination').indexOf(norm) === -1) {
csdlog = true;
return false; // break
}
});
}
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
usertalk: notifyuser,
welcomeuser: welcomeuser,
lognomination: csdlog
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging page");
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.main);
};
/*
****************************************
*** twinkleunlink.js: Unlink module
****************************************
* Mode of invocation: Tab ("Unlink")
* Active on: Non-special pages
* Config directives in: TwinkleConfig
*/
Twinkle.unlink = function twinkleunlink() {
if( mw.config.get('wgNamespaceNumber') < 0 ) {
return;
}
twAddPortletLink( Twinkle.unlink.callback, "Unlink", "tw-unlink", "Unlink backlinks" );
};
Twinkle.unlink.getChecked2 = function twinkleunlinkGetChecked2( nodelist ) {
if( !( nodelist instanceof NodeList ) && !( nodelist instanceof HTMLCollection ) ) {
return nodelist.checked ? [ nodelist.values ] : [];
}
var result = [];
for(var i = 0; i < nodelist.length; ++i ) {
if( nodelist[i].checked ) {
result.push( nodelist[i].values );
}
}
return result;
};
// the parameter is used when invoking unlink from admin speedy
Twinkle.unlink.callback = function(presetReason) {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Unlink backlinks" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#unlink" );
var form = new Morebits.quickForm( Twinkle.unlink.callback.evaluate );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: ',
value: (presetReason ? presetReason : '')
} );
var query;
if(mw.config.get('wgNamespaceNumber') === 6) { // File:
query = {
'action': 'query',
'list': [ 'backlinks', 'imageusage' ],
'bltitle': mw.config.get('wgPageName'),
'iutitle': mw.config.get('wgPageName'),
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'iulimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages.
};
} else {
query = {
'action': 'query',
'list': 'backlinks',
'bltitle': mw.config.get('wgPageName'),
'blfilterredir': 'nonredirects',
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages.
};
}
var wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.unlink.callbacks.display.backlinks );
wikipedia_api.params = { form: form, Window: Window, image: mw.config.get('wgNamespaceNumber') === 6 };
wikipedia_api.post();
var root = document.createElement( 'div' );
root.style.padding = '15px'; // just so it doesn't look broken
Morebits.status.init( root );
wikipedia_api.statelem.status( "loading..." );
Window.setContent( root );
Window.display();
};
Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
Twinkle.unlink.backlinksdone = 0;
Twinkle.unlink.imageusagedone = 0;
function processunlink(pages, imageusage) {
var statusIndicator = new Morebits.status((imageusage ? 'Unlinking instances of file usage' : 'Unlinking backlinks'), '0%');
var total = pages.length; // removing doubling of this number - no apparent reason for it
Morebits.wiki.addCheckpoint();
if( !pages.length ) {
statusIndicator.info( '100% (completed)' );
Morebits.wiki.removeCheckpoint();
return;
}
// get an edit token
var params = { reason: reason, imageusage: imageusage, globalstatus: statusIndicator, current: 0, total: total };
for (var i = 0; i < pages.length; ++i)
{
var myparams = $.extend({}, params);
var articlepage = new Morebits.wiki.page(pages[i], 'Unlinking in article "' + pages[i] + '"');
articlepage.setCallbackParameters(myparams);
articlepage.load(imageusage ? Twinkle.unlink.callbacks.unlinkImageInstances : Twinkle.unlink.callbacks.unlinkBacklinks);
}
}
var reason = event.target.reason.value;
var backlinks, imageusage;
if( event.target.backlinks ) {
backlinks = Twinkle.unlink.getChecked2(event.target.backlinks);
}
if( event.target.imageusage ) {
imageusage = Twinkle.unlink.getChecked2(event.target.imageusage);
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( event.target );
Morebits.wiki.addCheckpoint();
if (backlinks) {
processunlink(backlinks, false);
}
if (imageusage) {
processunlink(imageusage, true);
}
Morebits.wiki.removeCheckpoint();
};
Twinkle.unlink.backlinksdone = 0;
Twinkle.unlink.imageusagedone = 0;
Twinkle.unlink.callbacks = {
display: {
backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) {
var xmlDoc = apiobj.responseXML;
var havecontent = false;
var list, namespaces, i;
if( apiobj.params.image ) {
var imageusage = $(xmlDoc).find('query imageusage iu');
list = [];
for ( i = 0; i < imageusage.length; ++i ) {
var usagetitle = imageusage[i].getAttribute('title');
list.push( { label: usagetitle, value: usagetitle, checked: true } );
}
if (!list.length)
{
apiobj.params.form.append( { type: 'div', label: 'No instances of file usage found.' } );
}
else
{
apiobj.params.form.append( { type:'header', label: 'File usage' } );
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) {
namespaces.push(Morebits.wikipedia.namespacesFriendly[v]);
});
apiobj.params.form.append( {
type: 'div',
label: "Selected namespaces: " + namespaces.join(', '),
tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]"
});
if ($(xmlDoc).find('query-continue').length) {
apiobj.params.form.append( {
type: 'div',
label: "First " + list.length.toString() + " file usages shown."
});
}
apiobj.params.form.append( {
type: 'checkbox',
name: 'imageusage',
list: list
} );
havecontent = true;
}
}
var backlinks = $(xmlDoc).find('query backlinks bl');
if( backlinks.length > 0 ) {
list = [];
for ( i = 0; i < backlinks.length; ++i ) {
var title = backlinks[i].getAttribute('title');
list.push( { label: title, value: title, checked: true } );
}
apiobj.params.form.append( { type:'header', label: 'Backlinks' } );
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) {
namespaces.push(Morebits.wikipedia.namespacesFriendly[v]);
});
apiobj.params.form.append( {
type: 'div',
label: "Selected namespaces: " + namespaces.join(', '),
tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]"
});
if ($(xmlDoc).find('query-continue').length) {
apiobj.params.form.append( {
type: 'div',
label: "First " + list.length.toString() + " backlinks shown."
});
}
apiobj.params.form.append( {
type: 'checkbox',
name: 'backlinks',
list: list
});
havecontent = true;
}
else
{
apiobj.params.form.append( { type: 'div', label: 'No backlinks found.' } );
}
if (havecontent) {
apiobj.params.form.append( { type:'submit' } );
}
var result = apiobj.params.form.render();
apiobj.params.Window.setContent( result );
}
},
unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) {
var text, oldtext;
text = oldtext = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var wikiPage = new Morebits.wikitext.page(text);
wikiPage.removeLink(mw.config.get('wgPageName'));
text = wikiPage.getText();
if (text === oldtext) {
// Nothing to do, return
Twinkle.unlink.callbacks.success(pageobj);
Morebits.wiki.actionCompleted();
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Removing link(s) to \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.unlink.callbacks.success);
},
unlinkImageInstances: function twinkleunlinkCallbackUnlinkImageInstances(pageobj) {
var text, oldtext;
text = oldtext = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var wikiPage = new Morebits.wikitext.page(text);
wikiPage.commentOutImage(mw.config.get('wgTitle'), 'Commented out');
text = wikiPage.getText();
if (text === oldtext) {
// Nothing to do, return
Twinkle.unlink.callbacks.success(pageobj);
Morebits.wiki.actionCompleted();
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Commenting out use(s) of file \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.unlink.callbacks.success);
},
success: function twinkleunlinkCallbackSuccess(pageobj) {
var params = pageobj.getCallbackParameters();
var total = params.total;
var now = parseInt( 100 * (params.imageusage ? ++(Twinkle.unlink.imageusagedone) : ++(Twinkle.unlink.backlinksdone))/total, 10 ) + '%';
params.globalstatus.update( now );
if((params.imageusage ? Twinkle.unlink.imageusagedone : Twinkle.unlink.backlinksdone) >= total) {
params.globalstatus.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
}
};
/*
****************************************
*** twinklewarn.js: Warn module
****************************************
* Mode of invocation: Tab ("Warn")
* Active on: User talk pages
* Config directives in: TwinkleConfig
*/
Twinkle.warn = function twinklewarn() {
if( mw.config.get('wgNamespaceNumber') === 3 ) {
//twAddPortletLink( Twinkle.warn.callback, "Warn", "tw-warn", "Warn/notify user" );
}
// modify URL of talk page on rollback success pages
if( mw.config.get('wgAction') === 'rollback' ) {
var $vandalTalkLink = $("#mw-rollback-success").find(".mw-usertoollinks a").first();
$vandalTalkLink.css("font-weight", "bold");
$vandalTalkLink.wrapInner($("<span/>").attr("title", "If appropriate, you can use Twinkle to warn the user about their edits to this page."));
var extraParam = "vanarticle=" + mw.util.rawurlencode(mw.config.get("wgPageName").replace(/_/g, " "));
var href = $vandalTalkLink.attr("href");
if (href.indexOf("?") === -1) {
$vandalTalkLink.attr("href", href + "?" + extraParam);
} else {
$vandalTalkLink.attr("href", href + "&" + extraParam);
}
}
};
Twinkle.warn.callback = function twinklewarnCallback() {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
if( mw.config.get('wgTitle').split( '/' )[0] === mw.config.get('wgUserName') &&
!confirm( 'Warning yourself can be seen as a sign of mental instability! Are you sure you want to proceed?' ) ) {
return;
}
var Window = new Morebits.simpleWindow( 600, 440 );
Window.setTitle( "Warn/notify user" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "User talk page warnings", "Template:User_talk_page_warnings#Warnings_and_notices" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#warn" );
var form = new Morebits.quickForm( Twinkle.warn.callback.evaluate );
var main_select = form.append( {
type:'field',
label:'Choose type of warning/notice to issue',
tooltip:'First choose a main warning group, then the specific warning to issue.'
} );
var main_group = main_select.append( {
type:'select',
name:'main_group',
event:Twinkle.warn.callback.change_category
} );
var defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10);
main_group.append( { type:'option', label:'General note (1)', value:'level1', selected: ( defaultGroup === 1 || defaultGroup < 1 || ( Morebits.userIsInGroup( 'sysop' ) ? defaultGroup > 8 : defaultGroup > 7 ) ) } );
main_group.append( { type:'option', label:'Caution (2)', value:'level2', selected: ( defaultGroup === 2 ) } );
main_group.append( { type:'option', label:'Warning (3)', value:'level3', selected: ( defaultGroup === 3 ) } );
main_group.append( { type:'option', label:'Final warning (4)', value:'level4', selected: ( defaultGroup === 4 ) } );
main_group.append( { type:'option', label:'Only warning (4im)', value:'level4im', selected: ( defaultGroup === 5 ) } );
main_group.append( { type:'option', label:'Single issue notices', value:'singlenotice', selected: ( defaultGroup === 6 ) } );
main_group.append( { type:'option', label:'Single issue warnings', value:'singlewarn', selected: ( defaultGroup === 7 ) } );
if( Morebits.userIsInGroup( 'sysop' ) ) {
main_group.append( { type:'option', label:'Blocking', value:'block', selected: ( defaultGroup === 8 ) } );
}
main_select.append( { type:'select', name:'sub_group', event:Twinkle.warn.callback.change_subcategory } ); //Will be empty to begin with.
form.append( {
type:'input',
name:'article',
label:'Linked article',
value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ),
tooltip:'An article can be linked within the notice, perhaps because it was a revert to said article that dispatched this notice. Leave empty for no article to be linked.'
} );
var more = form.append( { type: 'field', name: 'reasonGroup', label: 'Warning information' } );
more.append( { type:'textarea', label:'Optional message:', name:'reason', tooltip:'Perhaps a reason, or that a more detailed notice must be appended' } );
var previewlink = document.createElement( 'a' );
$(previewlink).click(function(){
Twinkle.warn.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = "pointer";
previewlink.textContent = 'Preview';
more.append( { type: 'div', id: 'warningpreview', label: [ previewlink ] } );
more.append( { type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' } );
more.append( { type:'submit', label:'Submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
result.main_group.root = result;
result.previewer = new Morebits.wiki.preview($(result).find('div#twinklewarn-previewbox').last()[0]);
// We must init the first choice (General Note);
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.main_group.dispatchEvent( evt );
};
// This is all the messages that might be dispatched by the code
// Each of the individual templates require the following information:
// label (required): A short description displayed in the dialog
// summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with ". $summaryAd"
// suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such.
Twinkle.warn.messages = {
level1: {
"uw-vandalism1": {
label:"Vandalism",
summary:"General note: Unhelpful changes"
},
"uw-test1": {
label:"Editing tests",
summary:"General note: Editing tests"
},
"uw-delete1": {
label:"Removal of content, blanking",
summary:"General note: Removal of content, blanking"
},
"uw-create1": {
label:"Creating inappropriate pages",
summary:"General note: Creating inappropriate pages"
},
"uw-advert1": {
label:"Using Wikipedia for advertising or promotion",
summary:"General note: Using Wikipedia for advertising or promotion"
},
"uw-copyright1": {
label:"Copyright violation",
summary:"General note: Violating copyright"
},
"uw-error1": {
label:"Deliberately adding wrong information",
summary:"General note: Adding wrong information"
},
"uw-biog1": {
label:"Adding unreferenced controversial information about living persons",
summary:"General note: Adding unreferenced controversial information about living persons"
},
"uw-mos1": {
label:"Manual of style",
summary:"General note: Formatting, date, language, etc (Manual of style)"
},
"uw-move1": {
label:"Page moves against naming conventions or consensus",
summary:"General note: Page moves against naming conventions or consensus"
},
"uw-npov1": {
label:"Not adhering to neutral point of view",
summary:"General note: Not adhering to neutral point of view"
},
"uw-tpv1": {
label:"Changing others' talk page comments",
summary:"General note: Changing others' talk page comments"
},
"uw-qd": {
label:"Removing quick-deletion templates",
summary:"General note: Removing quick-deletion templates"
},
"uw-npa1": {
label:"Personal attack directed at another editor",
summary:"General note: Personal attack directed at another editor"
},
"uw-agf1": {
label:"Not assuming good faith",
summary:"General note: Not assuming good faith"
},
"uw-unsourced1": {
label:"Addition of unsourced or improperly cited material",
summary:"General note: Addition of unsourced or improperly cited material"
}
},
level2: {
"uw-vandalism2": {
label:"Vandalism",
summary:"Caution: Vandalism"
},
"uw-test2": {
label:"Editing tests",
summary:"Caution: Editing tests"
},
"uw-delete2": {
label:"Removal of content, blanking",
summary:"Caution: Removal of content, blanking"
},
"uw-create2": {
label:"Creating inappropriate pages",
summary:"Caution: Creating inappropriate pages"
},
"uw-advert2": {
label:"Using Wikipedia for advertising or promotion",
summary:"Caution: Using Wikipedia for advertising or promotion"
},
"uw-copyright2": {
label:"Copyright violation",
summary:"Caution: Violating copyright"
},
"uw-npov2": {
label:"Not adhering to neutral point of view",
summary:"Caution: Not adhering to neutral point of view"
},
"uw-error2": {
label:"Deliberately adding wrong information",
summary:"Caution: Adding wrong information"
},
"uw-biog2": {
label:"Adding unreferenced controversial information about living persons",
summary:"Caution: Adding unreferenced controversial information about living persons"
},
"uw-mos2": {
label:"Manual of style",
summary:"Caution: Formatting, date, language, etc (Manual of style)"
},
"uw-move2": {
label:"Page moves against naming conventions or consensus",
summary:"Caution: Page moves against naming conventions or consensus"
},
"uw-tpv2": {
label:"Changing others' talk page comments",
summary:"Caution: Changing others' talk page comments"
},
"uw-npa2": {
label:"Personal attack directed at another editor",
summary:"Caution: Personal attack directed at another editor"
},
"uw-agf2": {
label:"Not assuming good faith",
summary:"Caution: Not assuming good faith"
},
"uw-unsourced2": {
label:"Addition of unsourced or improperly cited material",
summary:"Caution: Addition of unsourced or improperly cited material"
}
},
level3: {
"uw-vandalism3": {
label:"Vandalism",
summary:"Warning: Vandalism"
},
"uw-test3": {
label:"Editing tests",
summary:"Warning: Editing tests"
},
"uw-delete3": {
label:"Removal of content, blanking",
summary:"Warning: Removal of content, blanking"
},
"uw-create3": {
label:"Creating inappropriate pages",
summary:"Warning: Creating inappropriate pages"
},
"uw-advert3": {
label:"Using Wikipedia for advertising or promotion",
summary:"Warning: Using Wikipedia for advertising or promotion"
},
"uw-npov3": {
label:"Not adhering to neutral point of view",
summary:"Warning: Not adhering to neutral point of view"
},
"uw-error3": {
label:"Deliberately adding wrong information",
summary:"Warning: Adding wrong information"
},
"uw-biog3": {
label:"Adding unreferenced controversial or defamatory information about living persons",
summary:"Warning: Adding unreferenced controversial information about living persons"
},
"uw-mos3": {
label:"Manual of style",
summary:"Warning: Formatting, date, language, etc (Manual of style)"
},
"uw-move3": {
label:"Page moves against naming conventions or consensus",
summary:"Warning: Page moves against naming conventions or consensus"
},
"uw-tpv3": {
label:"Changing others' talk page comments",
summary:"Warning: Changing others' talk page comments"
},
"uw-npa3": {
label:"Personal attack directed at another editor",
summary:"Warning: Personal attack directed at another editor"
},
"uw-agf3": {
label:"Not assuming good faith",
summary:"Warning: Not assuming good faith"
}
},
level4: {
"uw-generic4": {
label:"Generic warning (for template series missing level 4)",
summary:"Final warning notice"
},
"uw-vandalism4": {
label:"Vandalism",
summary:"Final warning: Vandalism"
},
"uw-test4": {
label:"Editing tests",
summary:"Final warning: Editing tests"
},
"uw-delete4": {
label:"Removal of content, blanking",
summary:"Final warning: Removal of content, blanking"
},
"uw-create4": {
label:"Creating inappropriate pages",
summary:"Final warning: Creating inappropriate pages"
},
"uw-advert4": {
label:"Using Wikipedia for advertising or promotion",
summary:"Final warning: Using Wikipedia for advertising or promotion"
},
"uw-npov4": {
label:"Not adhering to neutral point of view",
summary:"Final warning: Not adhering to neutral point of view"
},
"uw-error4": {
label:"Deliberately adding wrong information",
summary:"Final Warning: Adding wrong information"
},
"uw-biog4": {
label:"Adding unreferenced defamatory information about living persons",
summary:"Final warning: Adding unreferenced controversial information about living persons"
},
"uw-mos4": {
label:"Manual of style",
summary:"Final warning: Formatting, date, language, etc (Manual of style)"
},
"uw-move4": {
label:"Page moves against naming conventions or consensus",
summary:"Final warning: Page moves against naming conventions or consensus"
},
"uw-npa4": {
label:"Personal attack directed at another editor",
summary:"Final warning: Personal attack directed at another editor"
}
},
level4im: {
"uw-vandalism4im": {
label:"Vandalism",
summary:"Only warning: Vandalism"
},
"uw-delete4im": {
label:"Removal of content, blanking",
summary:"Only warning: Removal of content, blanking"
},
"uw-create4im": {
label:"Creating inappropriate pages",
summary:"Only warning: Creating inappropriate pages"
},
"uw-biog4im": {
label:"Adding unreferenced defamatory information about living persons",
summary:"Only warning: Adding unreferenced controversial information about living persons"
},
"uw-move4im": {
label:"Page moves against naming conventions or consensus",
summary:"Only warning: Page moves against naming conventions or consensus"
},
"uw-npa4im": {
label:"Personal attack directed at another editor",
summary:"Only warning: Personal attack directed at another editor"
}
},
singlenotice: {
"uw-badcat": {
label:"Adding incorrect categories",
summary:"Notice: Adding incorrect categories"
},
"uw-bite": {
label:"\"Biting\" newcomers",
summary:"Notice: \"Biting\" newcomers"
},
"uw-coi": {
label:"Possible conflict of interest",
summary:"Notice: Possible conflict of interest"
},
"uw-encopypaste": {
label:"Direct copying of article from English Wikipedia",
summary:"Notice: Direct copying of article from English Wikipedia"
},
"uw-encopyright": {
label:"Not giving attribution for content from another Wikipedia",
summary:"Notice: Reusing content from English Wikipedia without attribution"
},
"uw-emptycat": {
label:"Category created does not contain enough pages",
summary:"Notice: Creating empty categories"
},
"uw-joke": {
label:"Using improper humor",
summary:"Notice: Using improper humor"
},
"uw-lang": {
label:"Changing between types of English without a good reason",
summary:"Notice: Unnecessarily changing between British and American English"
},
"uw-newarticle": {
label:"Tips on creating new articles",
summary:"Notice: How to make your articles better"
},
"uw-notenglish": {
label:"Changes not in English",
summary:"Notice: Please edit in English"
},
"uw-otherweb": {
label:"Use \"Other websites\", not \"External links\"",
summary:"Notice: Use \"Other websites\", not \"External links\""
},
"uw-sandbox": {
label:"Removing the sandbox header",
summary:"Notice: Do not remove sandbox header"
},
"uw-selfrevert": {
label:"Undoing recent test",
summary:"Notice: Undoing recent test"
},
"uw-simple": {
label:"Not making changes in simple English",
summary:"Notice: Not making changes in simple English"
},
"uw-spellcheck": {
label:"Review spelling, etc.",
summary:"Notice: Review spelling, etc."
},
"uw-subst": {
label:"Remember to subst: templates",
summary:"Notice: Remember to subst: templates"
},
"uw-tilde": {
label:"Not signing posts",
summary:"Notice: Not signing posts"
},
"uw-upload": {
label:"Image uploads not allowed in Simple English Wikipedia",
summary:"Notice: Image uploads not allowed in Simple English Wikipedia"
},
"uw-warn": {
label:"Use user warn templates",
summary:"Notice: Use user warn templates"
}
},
singlewarn: {
"uw-3rr": {
label:"Edit warring",
summary:"Warning: Involved in edit war"
},
"uw-attack": {
label:"Creating attack pages",
summary:"Warning: Creating attack pages"
},
"uw-cyberbully": {
label:"Cyberbullying",
summary:"Warning: Cyberbullying"
},
"uw-disruption": {
label:"Project disruption",
summary:"Warning: Project disruption"
},
"uw-longterm": {
label:"Long term abuse",
summary:"Warning: Long term abuse"
},
"uw-qd": {
label:"Removing quick deletion templates from articles",
summary:"Warning: Removing quick deletion templates from articles"
},
"uw-spam": {
label:"Adding spam links",
summary:"Warning: Adding spam links"
},
"uw-userpage": {
label:"Userpage or subpage is against policy",
summary:"Warning: Userpage or subpage is against policy"
}
},
block: {
"uw-block1": {
label: "Block level 1",
summary: "You have been temporarily blocked",
reasonParam: true
},
"uw-block2": {
label: "Block level 2",
summary: "You have been blocked",
reasonParam: true
},
"uw-block3": {
label: "Block level 3",
summary: "You have been indefinitely blocked",
reasonParam: true
},
"UsernameBlocked": {
label: "Username block",
summary: "You have been blocked for violation of the [[Wikipedia:Username|username policy]]",
reasonParam: true
},
"UsernameHardBlocked": {
label: "Username hard block",
summary: "You have been blocked for a blatant violation of the [[Wikipedia:Username|username policy]]",
reasonParam: true
},
"Blocked proxy": {
label: "Blocked proxy",
summary: "You have been blocked because this IP is an [[open proxy]]"
},
"Uw-spamblock": {
label: "Spam block",
summary: "You have been blocked for [[Wikipedia:Spam|advertising or promotion]]"
},
"Cyberbully block": {
label: "Cyberbully block",
summary: "You have been blocked for [[Wikipedia:Cyberbullying|cyberbullying]]"
},
"Talkpage-revoked": {
label: "Talk-page access removed",
summary: "Your ability to change this [[Wikipedia:Talk page|talk page]] has been removed"
}
}
};
Twinkle.warn.prev_block_timer = null;
Twinkle.warn.prev_block_reason = null;
Twinkle.warn.prev_article = null;
Twinkle.warn.prev_reason = null;
Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) {
var value = e.target.value;
var sub_group = e.target.root.sub_group;
var messages = Twinkle.warn.messages[ value ];
sub_group.main_group = value;
var old_subvalue = sub_group.value;
var old_subvalue_re;
if( old_subvalue ) {
old_subvalue = old_subvalue.replace(/\d*(im)?$/, '' );
old_subvalue_re = new RegExp( RegExp.escape( old_subvalue ) + "(\\d*(?:im)?)$" );
}
while( sub_group.hasChildNodes() ){
sub_group.removeChild( sub_group.firstChild );
}
for( var i in messages ) {
var selected = false;
if( old_subvalue && old_subvalue_re.test( i ) ) {
selected = true;
}
var elem = new Morebits.quickForm.element( { type:'option', label:"{{" + i + "}}: " + messages[i].label, value:i, selected: selected } );
sub_group.appendChild( elem.render() );
}
if( value === 'block' ) {
// create the block-related fields
var more = new Morebits.quickForm.element( { type: 'div', id: 'block_fields' } );
more.append( {
type: 'input',
name: 'block_timer',
label: 'Period of blocking / Host ',
tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc... If you selected "blocked proxy", this text box will append the host name of the server'
} );
more.append( {
type: 'input',
name: 'block_reason',
label: '"You have been blocked for ..." ',
tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.'
} );
e.target.root.insertBefore( more.render(), e.target.root.lastChild );
// restore saved values of fields
if(Twinkle.warn.prev_block_timer !== null) {
e.target.root.block_timer.value = Twinkle.warn.prev_block_timer;
Twinkle.warn.prev_block_timer = null;
}
if(Twinkle.warn.prev_block_reason !== null) {
e.target.root.block_reason.value = Twinkle.warn.prev_block_reason;
Twinkle.warn.prev_block_reason = null;
}
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.root.article.value;
}
e.target.root.article.disabled = false;
$(e.target.root.reason).parent().hide();
e.target.root.previewer.closePreview();
} else if( e.target.root.block_timer ) {
// hide the block-related fields
if(!e.target.root.block_timer.disabled && Twinkle.warn.prev_block_timer === null) {
Twinkle.warn.prev_block_timer = e.target.root.block_timer.value;
}
if(!e.target.root.block_reason.disabled && Twinkle.warn.prev_block_reason === null) {
Twinkle.warn.prev_block_reason = e.target.root.block_reason.value;
}
$(e.target.root).find("#block_fields").remove();
if(e.target.root.article.disabled && Twinkle.warn.prev_article !== null) {
e.target.root.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.root.article.disabled = false;
$(e.target.root.reason).parent().show();
e.target.root.previewer.closePreview();
}
// clear overridden label on article textbox
Morebits.quickForm.setElementTooltipVisibility(e.target.root.article, true);
Morebits.quickForm.resetElementLabel(e.target.root.article);
};
Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) {
var main_group = e.target.form.main_group.value;
var value = e.target.form.sub_group.value;
if( main_group === 'singlewarn' ) {
if( value === 'uw-username' ) {
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.notArticle = true;
e.target.form.article.value = '';
} else if( e.target.form.article.notArticle ) {
if(Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.notArticle = false;
}
} else if( main_group === 'block' ) {
if( Twinkle.warn.messages.block[value].indefinite ) {
if(Twinkle.warn.prev_block_timer === null) {
Twinkle.warn.prev_block_timer = e.target.form.block_timer.value;
}
e.target.form.block_timer.disabled = true;
e.target.form.block_timer.value = 'indefinite';
} else if( e.target.form.block_timer.disabled ) {
if(Twinkle.warn.prev_block_timer !== null) {
e.target.form.block_timer.value = Twinkle.warn.prev_block_timer;
Twinkle.warn.prev_block_timer = null;
}
e.target.form.block_timer.disabled = false;
}
if( Twinkle.warn.messages.block[value].pageParam ) {
if(Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.disabled = false;
} else if( !e.target.form.article.disabled ) {
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.disabled = true;
e.target.form.article.value = '';
}
if( Twinkle.warn.messages.block[value].reasonParam ) {
if(Twinkle.warn.prev_block_reason !== null) {
e.target.form.block_reason.value = Twinkle.warn.prev_block_reason;
Twinkle.warn.prev_block_reason = null;
}
e.target.form.block_reason.disabled = false;
} else if( !e.target.form.block_reason.disabled ) {
if(Twinkle.warn.prev_block_reason === null) {
Twinkle.warn.prev_block_reason = e.target.form.block_reason.value;
}
e.target.form.block_reason.disabled = true;
e.target.form.block_reason.value = '';
}
}
// change form labels according to the warning selected
if (value === "uw-username") {
Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, false);
Morebits.quickForm.overrideElementLabel(e.target.form.article, "Username violates policy because... ");
} else {
Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, true);
Morebits.quickForm.resetElementLabel(e.target.form.article);
}
};
Twinkle.warn.callbacks = {
preview: function(form) {
var templatename = form.sub_group.value;
var templatetext = '{{subst:' + templatename;
var linkedarticle = form.article.value;
if (templatename in Twinkle.warn.messages.block) {
if( linkedarticle && Twinkle.warn.messages.block[templatename].pageParam ) {
templatetext += '|page=' + linkedarticle;
}
var blocktime = form.block_timer.value;
if( /te?mp|^\s*$|min/.exec( blocktime ) || Twinkle.warn.messages.block[templatename].indefinite ) {
; // nothing
} else if( /indef|\*|max/.exec( blocktime ) ) {
templatetext += '|indef=yes';
} else {
templatetext += '|host=' + blocktime;
templatetext += '|time=' + blocktime;
}
var blockreason = form.block_reason.value;
if( blockreason ) {
templatetext += '|reason=' + blockreason;
}
templatetext += "|sig=true}}";
} else {
if (linkedarticle) {
// add linked article for user warnings (non-block templates)
templatetext += '|1=' + linkedarticle;
}
templatetext += '}}';
// add extra message for non-block templates
var reason = form.reason.value;
if (reason) {
templatetext += " ''" + reason + "''";
}
}
form.previewer.beginRender(templatetext);
},
main: function( pageobj ) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var messageData = Twinkle.warn.messages[params.main_group][params.sub_group];
var history_re = /<!-- Template:(uw-.*?) -->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4}) \(UTC\)/g;
var history = {};
var latest = { date:new Date( 0 ), type:'' };
var current;
while( ( current = history_re.exec( text ) ) ) {
var current_date = new Date( current[2] + ' UTC' );
if( !( current[1] in history ) || history[ current[1] ] < current_date ) {
history[ current[1] ] = current_date;
}
if( current_date > latest.date ) {
latest.date = current_date;
latest.type = current[1];
}
}
var date = new Date();
if( params.sub_group in history ) {
var temp_time = new Date( history[ params.sub_group ] );
temp_time.setUTCHours( temp_time.getUTCHours() + 24 );
if( temp_time > date ) {
if( !confirm( "An identical " + params.sub_group + " has been issued in the last 24 hours. \nWould you still like to add this warning/notice?" ) ) {
pageobj.statelem.info( 'aborted per user request' );
return;
}
}
}
latest.date.setUTCMinutes( latest.date.getUTCMinutes() + 1 ); // after long debate, one minute is max
if( latest.date > date ) {
if( !confirm( "A " + latest.type + " has been issued in the last minute. \nWould you still like to add this warning/notice?" ) ) {
pageobj.statelem.info( 'aborted per user request' );
return;
}
}
var mainheaderRe = new RegExp("==+\\s*Warnings\\s*==+");
var headerRe = new RegExp( "^==+\\s*(?:" + date.getUTCMonthName() + '|' + date.getUTCMonthNameAbbrev() + ")\\s+" + date.getUTCFullYear() + "\\s*==+", 'm' );
if( text.length > 0 ) {
text += "\n\n";
}
if( params.main_group === 'block' ) {
var article = '', reason = '', host = '', time = null;
if( Twinkle.getPref('blankTalkpageOnIndefBlock') && params.sub_group !== 'uw-lblock' && ( Twinkle.warn.messages.block[params.sub_group].indefinite || (/indef|\*|max/).exec( params.block_timer ) ) ) {
Morebits.status.info( 'Info', 'Blanking talk page per preferences and creating a new level 2 heading for the date' );
text = "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
} else if( !headerRe.exec( text ) ) {
Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' );
text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
}
if( params.reason && Twinkle.warn.messages.block[params.sub_group].reasonParam ) {
reason = '|reason=' + params.reason;
}
if( /te?mp|^\s*$|min/.exec( params.block_timer ) || Twinkle.warn.messages.block[params.sub_group].indefinite ) {
time = '';
} else if( /indef|\*|max/.exec( params.block_timer ) ) {
time = '|indef=yes';
} else {
time = '|time=' + params.block_timer;
}
if ( params.sub_group === "Blocked proxy" )
{
text += "{{" + params.sub_group + "|host=" + params.block_timer + "}}";
} else {
text += "{{subst:" + params.sub_group + time + reason + "|sig=yes}}";
}
} else {
if( !headerRe.exec( text ) ) {
Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' );
text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
}
text += "{{subst:" + params.sub_group + ( params.article ? '|1=' + params.article : '' ) + "|subst=subst:}}" + (params.reason ? " ''" + params.reason + "'' ": ' ' ) + "~~~~";
}
if ( Twinkle.getPref('showSharedIPNotice') && Morebits.isIPAddress( mw.config.get('wgTitle') ) ) {
Morebits.status.info( 'Info', 'Adding a shared IP notice' );
text += "\n{{subst:SharedIPAdvice}}";
}
var summary = messageData.summary;
if ( messageData.suppressArticleInSummary !== true && params.article ) {
summary += " on [[" + params.article + "]]";
}
summary += "." + Twinkle.getPref("summaryAd");
pageobj.setPageText( text );
pageobj.setEditSummary( summary );
pageobj.setWatchlist( Twinkle.getPref('watchWarnings') );
pageobj.save();
}
};
Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) {
// First, check to make sure a reason was filled in if uw-username was selected
if(e.target.sub_group.value === 'uw-username' && e.target.article.value.trim() === '') {
alert("You must supply a reason for the {{uw-username}} template.");
return;
}
// Then, grab all the values provided by the form
var params = {
reason: e.target.block_reason ? e.target.block_reason.value : e.target.reason.value,
main_group: e.target.main_group.value,
sub_group: e.target.sub_group.value,
article: e.target.article.value, // .replace( /^(Image|Category):/i, ':$1:' ), -- apparently no longer needed...
block_timer: e.target.block_timer ? e.target.block_timer.value : null
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Warning complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page( mw.config.get('wgPageName'), 'User talk page modification' );
wikipedia_page.setCallbackParameters( params );
wikipedia_page.setFollowRedirect( true );
wikipedia_page.load( Twinkle.warn.callbacks.main );
};
/*
****************************************
*** twinklexfd.js: XFD module
****************************************
* Mode of invocation: Tab ("XFD")
* Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects
* Config directives in: TwinkleConfig
*/
Twinkle.xfd = function twinklexfd() {
// Disable on:
// * special pages
// * non-existent pages
// * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2)
// * file pages without actual files (these are eligible for CSD G8)
if ( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && (document.getElementById('mw-sharedupload') || (!document.getElementById('mw-imagepage-section-filehistory') && !Morebits.wiki.isPageRedirect()))) ) {
return;
}
//twAddPortletLink( Twinkle.xfd.callback, "RfD", "tw-xfd", "Nominate for deletion" );
};
Twinkle.xfd.num2order = function twinklexfdNum2order( num ) {
switch( num ) {
case 1: return '';
case 2: return '2nd';
case 3: return '3rd';
default: return num + 'th';
}
};
Twinkle.xfd.currentRationale = null;
// error callback on Morebits.status.object
Twinkle.xfd.printRationale = function twinklexfdPrintRationale() {
if (Twinkle.xfd.currentRationale) {
var p = document.createElement("p");
p.textContent = "Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:";
var pre = document.createElement("pre");
pre.className = "toccolours";
pre.style.marginTop = "0";
pre.textContent = Twinkle.xfd.currentRationale;
p.appendChild(pre);
Morebits.status.root.appendChild(p);
// only need to print the rationale once
Twinkle.xfd.currentRationale = null;
}
};
Twinkle.xfd.callback = function twinklexfdCallback() {
if (!twinkleUserAuthorized) {
alert("Your account is too new to use Twinkle.");
return;
}
var Window = new Morebits.simpleWindow( 600, 350 );
Window.setTitle( "Nominate for deletion (RfD)" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Deletion policy", "Wikipedia:Deletion policy" );
Window.addFooterLink( "About deletion discussions", "WP:RfD" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#xfd" );
var form = new Morebits.quickForm( Twinkle.xfd.callback.evaluate );
var categories = form.append( {
type: 'select',
name: 'category',
label: 'Select wanted type of category: ',
tooltip: 'This default should be the most appropriate, as no other deletion discussion pages exist here.',
event: Twinkle.xfd.callback.change_category
} );
categories.append( {
type: 'option',
label: 'RfD (Requests for deletion)',
selected: true,
value: 'afd'
} );
form.append( {
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notify',
tooltip: "A notification template will be placed on the creator's talk page if this is true.",
checked: true
}
]
}
);
form.append( {
type: 'field',
label:'Work area',
name: 'work_area'
} );
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the controls
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.category.dispatchEvent( evt );
};
Twinkle.xfd.previousNotify = true;
Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) {
var value = e.target.value;
var form = e.target.form;
var old_area = Morebits.quickForm.getElements(e.target.form, "work_area")[0];
var work_area = null;
var oldreasontextbox = form.getElementsByTagName('textarea')[0];
var oldreason = (oldreasontextbox ? oldreasontextbox.value : '');
work_area = new Morebits.quickForm.element( {
type: 'field',
label: 'Requests for deletion',
name: 'work_area'
} );
work_area.append( {
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. This option is not normally required.'
}
]
} );
work_area.append( {
type: 'textarea',
name: 'xfdreason',
label: 'Reason: '
} );
work_area = work_area.render();
old_area.parentNode.replaceChild( work_area, old_area );
}
Twinkle.xfd.callbacks = {
afd: {
main: function(apiobj) {
var xmlDoc = apiobj.responseXML;
var titles = $(xmlDoc).find('allpages p');
// There has been no earlier entries with this prefix, just go on.
if( titles.length <= 0 ) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
var number = 0;
for( var i = 0; i < titles.length; ++i ) {
var title = titles[i].getAttribute('title');
// First, simple test, is there an instance with this exact name?
if( title === 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') ) {
number = Math.max( number, 1 );
continue;
}
var order_re = new RegExp( '^' +
RegExp.escape( 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), true ) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
var match = order_re.exec( title );
// No match; A non-good value
if( !match ) {
continue;
}
// A match, set number to the max of current
number = Math.max( number, Number(match[1]) );
}
apiobj.params.number = Twinkle.xfd.num2order( parseInt( number, 10 ) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + apiobj.params.numbering;
Morebits.status.info( "Next discussion page", "[[" + apiobj.params.discussionpage + "]]" );
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to the discussion page";
// Tagging article
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Adding deletion tag to article");
if(window.location.search.includes("redirect=no")) {
wikipedia_page.setFollowRedirect(false); // User's intention was probably to tag the redirect itself
} else {
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
}
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle);
},
// Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page
taggingArticle: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
// Check for existing AfD tag, for the benefit of new page patrollers
var textNoAfd = text.replace(/\{\{\s*(Requests for deletion\/dated|RfDM)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, "");
if (text !== textNoAfd) {
if (confirm("An RfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current RfD tag (not recommended), or Cancel to abandon your nomination.")) {
text = textNoAfd;
} else {
statelem.error("Article already tagged with RfD tag, and you chose to abort");
window.location.reload();
return;
}
}
// Now we know we want to go ahead with it, trigger the other AJAX requests
// Starting discussion page
var wikipedia_page = new Morebits.wiki.page(params.discussionpage, "Creating article deletion discussion page");
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage);
// Today's list
var date = new Date();
wikipedia_page = new Morebits.wiki.page('Wikipedia:Requests for deletion', "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList);
// Notification to first contributor
if (params.usertalk) {
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.lookupCreator(Twinkle.xfd.callbacks.afd.userNotification);
}
// Then, test if there are speedy deletion-related templates on the article.
var textNoSd = text.replace(/\{\{\s*(db(-\w*)?|qd|delete|(?:hang|hold)[\- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, "");
if (text !== textNoSd && confirm("A quick deletion tag was found on this page. Should it be removed?")) {
text = textNoSd;
}
pageobj.setPageText(( params.noinclude ? "<noinclude>" : "" ) + "\{\{RfD|" + params.reason + "\}\}\n" + ( params.noinclude ? "</noinclude>" : "" ) + text);
pageobj.setEditSummary("Nominated for deletion; see [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchPage')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('nocreate');
pageobj.save();
},
discussionPage: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
pageobj.setPageText("{{subst:RfD/Preload/Template|deletereason=" + params.reason + "}}\n");
pageobj.setEditSummary("Creating deletion discussion page for [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchDiscussion')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('createonly');
pageobj.save(function() {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
todaysList: function(pageobj) {
var old_text = pageobj.getPageText() + "\n"; // MW strips trailing blanks, but we like them, so we add a fake one
var params = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
var text = old_text.replace( /(<\!-- Add new entries to the TOP of the following list -->\n+)/, "$1{{Wikipedia:Requests for deletion/Requests/" + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + params.numbering + "}}\n");
if( text === old_text ) {
statelem.error( 'failed to find target spot for the discussion' );
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Adding [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchList')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('recreate');
pageobj.save();
},
userNotification: function(pageobj) {
var params = pageobj.getCallbackParameters();
var initialContrib = pageobj.getCreator();
var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")");
var notifytext = "\n{{subst:RFDNote|1=" + mw.config.get('wgPageName') + "|2=" + mw.config.get('wgPageName') + ( params.numbering !== '' ? '|order= ' + params.numbering : '' ) + "}} ~~~~";
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary("Notification: listing at [[WP:RfD|requests for deletion]] of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
usertalkpage.setCreateOption('recreate');
switch (Twinkle.getPref('xfdWatchUser')) {
case 'yes':
usertalkpage.setWatchlist(true);
break;
case 'no':
usertalkpage.setWatchlistFromPreferences(false);
break;
default:
usertalkpage.setWatchlistFromPreferences(true);
break;
}
usertalkpage.setFollowRedirect(true);
usertalkpage.append();
}
}
};
Twinkle.xfd.callback.evaluate = function(e) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var type = e.target.category.value;
var usertalk = e.target.notify.checked;
var reason = e.target.xfdreason.value;
var xfdtarget, xfdtarget2, puf, noinclude, tfdinline, notifyuserspace;
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Twinkle.xfd.currentRationale = reason;
Morebits.status.onError(Twinkle.xfd.printRationale);
if( !type ) {
Morebits.status.error( 'Error', 'no action given' );
return;
}
var query, wikipedia_page, wikipedia_api, logpage, params;
var date = new Date();
query = {
'action': 'query',
'list': 'allpages',
'apprefix': 'Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'),
'apnamespace': 4,
'apfilterredir': 'nonredirects',
'aplimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500
};
wikipedia_api = new Morebits.wiki.api( 'Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main );
wikipedia_api.params = { usertalk:usertalk, reason:reason, noinclude:noinclude };
wikipedia_api.post();
};
/**
* General initialization code
*/
var scriptpathbefore = "https://dev.miraheze.org" + mw.util.wikiScript( "index" ) + "?title=",
scriptpathafter = "&action=raw&ctype=text/javascript&happy=yes";
// Retrieve the user's Twinkle preferences
$.ajax({
url: scriptpathbefore + "User:" + encodeURIComponent( mw.config.get("wgUserName")) + "/twinkleoptions.js" + scriptpathafter,
dataType: "text",
error: function () { mw.notify( "Could not load twinkleoptions.js" ); },
success: function ( optionsText ) {
// Quick pass if user has no options
if ( optionsText === "" ) {
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace( /(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, "" );
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if ( optionsText.lastIndexOf( "window.Twinkle.prefs = ", 0 ) === 0 ) {
optionsText = optionsText.replace( /(?:^window.Twinkle.prefs = |;\n*$)/g, "" );
}
try {
var options = JSON.parse( optionsText );
// Assuming that our options evolve, we will want to transform older versions:
//if ( options.optionsVersion === undefined ) {
// ...
// options.optionsVersion = 1;
//}
//if ( options.optionsVersion === 1 ) {
// ...
// options.optionsVersion = 2;
//}
// At the same time, twinkleconfig.js needs to be adapted to write a higher version number into the options.
if ( options ) {
Twinkle.prefs = options;
}
}
catch ( e ) {
mw.notify("Could not parse twinkleoptions.js");
}
},
complete: function () {
$( Twinkle.load );
}
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function () {
// Don't activate on special pages other than "Contributions" so that they load faster, especially the watchlist.
// Also, Twinkle is incompatible with Internet Explorer versions 8 or lower, so don't load there either.
var specialPageWhitelist = [ 'Block', 'Contributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsInGroup('sysop')) {
specialPageWhitelist = specialPageWhitelist.concat([ 'DeletedContributions', 'Prefixindex' ]);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
specialPageWhitelist.indexOf(mw.config.get('wgCanonicalSpecialPageName')) === -1) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
if ($.client.profile().name === 'msie' && $.client.profile().versionNumber < 9) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.api.setApiUserAgent('Twinkle/2.0 (' + mw.config.get('wgDBname') + ')');
// Load the modules in the order that the tabs should appears
// Deletion
Twinkle.speedy();
// Misc. ones last
Twinkle.diff();
Twinkle.unlink();
Twinkle.config.init();
Twinkle.fluff.init();
if ( Morebits.userIsInGroup('sysop') ) {
Twinkle.batchdelete();
Twinkle.batchprotect();
Twinkle.batchundelete();
}
// Run the initialization callbacks for any custom modules
$( Twinkle.initCallbacks ).each(function ( k, v ) { v(); });
Twinkle.addInitCallback = function ( func ) { func(); };
// Increases text size in Twinkle dialogs, if so configured
if ( Twinkle.getPref( "dialogLargeFont" ) ) {
mw.util.addCSS( ".morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } " +
".morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }" );
}
};
} ( window, document, jQuery )); // End wrap with anonymous function
// </nowiki>
3399804aac47c3b253525d584e1077068e37daf4
MediaWiki:Gadget-morebits.js
8
82
156
2023-03-11T01:50:47Z
dev>Pppery
0
javascript
text/javascript
// <nowiki>
/**
* morebits.js
* ===========
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - Morebits.quickForm class - generates quick HTML forms on the fly
* - Morebits.wiki.api class - makes calls to the MediaWiki API
* - Morebits.wiki.page class - modifies pages on the wiki (edit, revert, delete, etc.)
* - Morebits.wikitext class - contains some utilities for dealing with wikitext
* - Morebits.status class - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - Morebits.simpleWindow class - a wrapper for jQuery UI Dialog with a custom look and extra features
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - Morebits.quickForm, Morebits.simpleWindow, and Morebits.status rely on the "morebits.css" file for their styling.
* - Morebits.simpleWindow relies on jquery UI Dialog (ResourceLoader module name 'jquery.ui').
* - Morebits.quickForm tooltips rely on Tipsy (ResourceLoader module name 'jquery.tipsy').
* For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy].
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* * GadgetName[ResourceLoader|dependencies=mediawiki.util,jquery.ui,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js
*
* Most of the stuff here doesn't work on IE < 9. It is your script's responsibility to enforce this.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[Wikipedia talk:Twinkle]] on English Wikipedia [http://en.wikipedia.org].
* The latest development source is available at [https://github.com/azatoth/twinkle/blob/master/morebits.js].
*
* From simplewiki
*/
( function ( window, document, $, undefined ) { // Wrap entire file with anonymous function
var Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* **************** Morebits.userIsInGroup() ****************
* Simple helper function to see what groups a user might belong
*/
Morebits.userIsInGroup = function ( group ) {
return $.inArray(group, mw.config.get( 'wgUserGroups' )) !== -1;
}
/**
* **************** Morebits.isIPAddress() ****************
* Helper function: Returns true if given string contains a valid IPv4 or
* IPv6 address
*/
Morebits.isIPAddress = function ( address ) {
return mw.util.isIPv4Address(address) || mw.util.isIPv6Address(address);
};
/**
* **************** Morebits.sanitizeIPv6() ****************
* JavaScript translation of the MediaWiki core function IP::sanitizeIP() in
* includes/utils/IP.php.
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
*/
Morebits.sanitizeIPv6 = function ( address ) {
address = address.trim();
if ( address === '' ) {
return null;
}
if ( mw.util.isIPv4Address( address ) || !mw.util.isIPv6Address( address ) ) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
var abbrevPos = address.indexOf( '::' );
if ( abbrevPos > -1 ) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
var CIDRStart = address.indexOf( '/' );
var addressEnd = ( CIDRStart > -1 ) ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
var repeat, extra, pad;
if ( abbrevPos === 0 ) {
repeat = '0:';
extra = ( address == '::' ) ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if ( abbrevPos === ( addressEnd - 1 ) ) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
var replacement = repeat;
pad -= address.split( ':' ).length - 1;
for ( var i = 1; i < pad; i++ ) {
replacement += repeat;
}
replacement += extra;
address = address.replace( '::', replacement );
}
// Remove leading zeros from each bloc as needed
address = address.replace( /(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2' );
return address;
};
/**
* **************** Morebits.quickForm ****************
* Morebits.quickForm is a class for creation of simple and standard forms without much
* specific coding.
*
* Index to Morebits.quickForm element types:
*
* select A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event
* option An element for a combo box.
* - Attributes: value, label, selected, disabled
* optgroup A group of "option"s.
* - Attributes: label, list
* field A fieldset (aka group box).
* - Attributes: name, label
* checkbox A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* radio A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* input A text box.
* - Attributes: name, label, value, size, disabled, readonly, maxlength, event
* dyninput A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, sublabel, value, size, maxlength, event
* hidden An invisible form field.
* - Attributes: name, value
* header A level 5 header.
* - Attributes: label
* div A generic placeholder element or label.
* - Attributes: name, label
* submit A submit button. Morebits.simpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* button A generic button.
* - Attributes: name, label, disabled, event
* textarea A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, readonly
*
* Global attributes: id, style, tooltip, extra, adminonly
*/
Morebits.quickForm = function QuickForm( event, eventType ) {
this.root = new Morebits.quickForm.element( { type: 'form', event: event, eventType:eventType } );
};
Morebits.quickForm.prototype.render = function QuickFormRender() {
var ret = this.root.render();
ret.names = {};
return ret;
};
Morebits.quickForm.prototype.append = function QuickFormAppend( data ) {
return this.root.append( data );
};
Morebits.quickForm.element = function QuickFormElement( data ) {
this.data = data;
this.childs = [];
this.id = Morebits.quickForm.element.id++;
};
Morebits.quickForm.element.id = 0;
Morebits.quickForm.element.prototype.append = function QuickFormElementAppend( data ) {
var child;
if( data instanceof Morebits.quickForm.element ) {
child = data;
} else {
child = new Morebits.quickForm.element( data );
}
this.childs.push( child );
return child;
};
// This should be called without parameters: form.render()
Morebits.quickForm.element.prototype.render = function QuickFormElementRender( internal_subgroup_id ) {
var currentNode = this.compute( this.data, internal_subgroup_id );
for( var i = 0; i < this.childs.length; ++i ) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild( this.childs[i].render() );
}
return currentNode[0];
};
Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute( data, in_id ) {
var node;
var childContainder = null;
var label;
var id = ( in_id ? in_id + '_' : '' ) + 'node_' + this.id;
if( data.adminonly && !Morebits.userIsInGroup( 'sysop' ) ) {
// hell hack alpha
data.type = 'hidden';
}
var i, current, subnode;
switch( data.type ) {
case 'form':
node = document.createElement( 'form' );
node.className = "quickform";
node.setAttribute( 'action', 'javascript:void(0);');
if( data.event ) {
node.addEventListener( data.eventType || 'submit', data.event , false );
}
break;
case 'select':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.setAttribute( 'for', id );
label.appendChild( document.createTextNode( data.label ) );
}
var select = node.appendChild( document.createElement( 'select' ) );
if( data.event ) {
select.addEventListener( 'change', data.event, false );
}
if( data.multiple ) {
select.setAttribute( 'multiple', 'multiple' );
}
if( data.size ) {
select.setAttribute( 'size', data.size );
}
select.setAttribute( 'name', data.name );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
current = data.list[i];
if( current.list ) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute( current );
select.appendChild( subnode[0] );
}
}
childContainder = select;
break;
case 'option':
node = document.createElement( 'option' );
node.values = data.value;
node.setAttribute( 'value', data.value );
if( data.selected ) {
node.setAttribute( 'selected', 'selected' );
}
if( data.disabled ) {
node.setAttribute( 'disabled', 'disabled' );
}
node.setAttribute( 'label', data.label );
node.appendChild( document.createTextNode( data.label ) );
break;
case 'optgroup':
node = document.createElement( 'optgroup' );
node.setAttribute( 'label', data.label );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
current = data.list[i];
current.type = 'option'; //must be options here
subnode = this.compute( current );
node.appendChild( subnode[0] );
}
}
break;
case 'field':
node = document.createElement( 'fieldset' );
label = node.appendChild( document.createElement( 'legend' ) );
label.appendChild( document.createTextNode( data.label ) );
if( data.name ) {
node.setAttribute( 'name', data.name );
}
break;
case 'checkbox':
case 'radio':
node = document.createElement( 'div' );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
var cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if( current.type === 'header' ) {
// inline hack
cur_div = node.appendChild( document.createElement( 'h6' ) );
cur_div.appendChild( document.createTextNode( current.label ) );
if( current.tooltip ) {
Morebits.quickForm.element.generateTooltip( cur_div , current );
}
continue;
}
cur_div = node.appendChild( document.createElement( 'div' ) );
subnode = cur_div.appendChild( document.createElement( 'input' ) );
subnode.values = current.value;
subnode.setAttribute( 'value', current.value );
subnode.setAttribute( 'name', current.name || data.name );
subnode.setAttribute( 'type', data.type );
subnode.setAttribute( 'id', cur_id );
if( current.checked ) {
subnode.setAttribute( 'checked', 'checked' );
}
if( current.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.event ) {
subnode.addEventListener( 'change', data.event, false );
} else if ( current.event ) {
subnode.addEventListener( 'change', current.event, true );
}
label = cur_div.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( current.label ) );
label.setAttribute( 'for', cur_id );
if( current.tooltip ) {
Morebits.quickForm.element.generateTooltip( label, current );
}
var event;
if( current.subgroup ) {
var tmpgroup = current.subgroup; // $.extend({}, current.subgroup); really needed?
if( ! $.isArray( tmpgroup ) ) {
tmpgroup = [ tmpgroup ];
}
var subgroupRaw = new Morebits.quickForm.element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each( tmpgroup, function( idx, el ) {
if( ! el.type ) {
el.type = data.type;
}
el.name = (current.name || data.name) + '.' + el.name;
subgroupRaw.append( el );
} );
var subgroup = subgroupRaw.render( cur_id );
subgroup.className = "quickformSubgroup";
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if( e.target.checked ) {
e.target.parentNode.appendChild( e.target.subgroup );
if( e.target.type === 'radio' ) {
var name = e.target.name;
if( e.target.form.names[name] !== undefined ) {
e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup );
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild( e.target.subgroup );
}
};
subnode.addEventListener( 'change', event, true );
if( current.checked ) {
subnode.parentNode.appendChild( subgroup );
}
} else if( data.type === 'radio' ) {
event = function(e) {
if( e.target.checked ) {
var name = e.target.name;
if( e.target.form.names[name] !== undefined ) {
e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup );
}
delete e.target.form.names[name];
}
};
subnode.addEventListener( 'change', event, true );
}
}
}
break;
case 'input':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( data.label ) );
label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'input' ) );
if( data.value ) {
subnode.setAttribute( 'value', data.value );
}
subnode.setAttribute( 'name', data.name );
subnode.setAttribute( 'id', id );
subnode.setAttribute( 'type', 'text' );
if( data.size ) {
subnode.setAttribute( 'size', data.size );
}
if( data.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.readonly ) {
subnode.setAttribute( 'readonly', 'readonly' );
}
if( data.maxlength ) {
subnode.setAttribute( 'maxlength', data.maxlength );
}
if( data.event ) {
subnode.addEventListener( 'keyup', data.event, false );
}
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement( 'div' );
label = node.appendChild( document.createElement( 'h5' ) );
label.appendChild( document.createTextNode( data.label ) );
var listNode = node.appendChild( document.createElement( 'div' ) );
var more = this.compute( {
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
var area = e.target.area;
var new_node = new Morebits.quickForm.element( e.target.sublist );
e.target.area.appendChild( new_node.render() );
if( ++e.target.counter >= e.target.max ) {
e.target.setAttribute( 'disabled', 'disabled' );
}
e.stopPropagation();
}
} );
node.appendChild( more[0] );
var moreButton = more[1];
var sublist = {
type: '_dyninput_element',
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size,
remove: false,
maxlength: data.maxlength,
event: data.event
};
for( i = 0; i < min; ++i ) {
var elem = new Morebits.quickForm.element( sublist );
listNode.appendChild( elem.render() );
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_element': // Private, similar to normal input
node = document.createElement( 'div' );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( data.label ) );
label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'input' ) );
if( data.value ) {
subnode.setAttribute( 'value', data.value );
}
subnode.setAttribute( 'name', data.name );
subnode.setAttribute( 'type', 'text' );
if( data.size ) {
subnode.setAttribute( 'size', data.size );
}
if( data.maxlength ) {
subnode.setAttribute( 'maxlength', data.maxlength );
}
if( data.event ) {
subnode.addEventListener( 'keyup', data.event, false );
}
if( data.remove ) {
var remove = this.compute( {
type: 'button',
label: 'remove',
event: function(e) {
var list = e.target.listnode;
var node = e.target.inputnode;
var more = e.target.morebutton;
list.removeChild( node );
--more.counter;
more.removeAttribute( 'disabled' );
e.stopPropagation();
}
} );
node.appendChild( remove[0] );
var removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case 'hidden':
node = document.createElement( 'input' );
node.setAttribute( 'type', 'hidden' );
node.values = data.value;
node.setAttribute( 'value', data.value );
node.setAttribute( 'name', data.name );
break;
case 'header':
node = document.createElement( 'h5' );
node.appendChild( document.createTextNode( data.label ) );
break;
case 'div':
node = document.createElement( 'div' );
if (data.name) {
node.setAttribute( 'name', data.name );
}
if (data.label) {
if ( ! $.isArray( data.label ) ) {
data.label = [ data.label ];
}
var result = document.createElement( 'span' );
result.className = 'quickformDescription';
for( i = 0; i < data.label.length; ++i ) {
if( typeof data.label[i] === 'string' ) {
result.appendChild( document.createTextNode( data.label[i] ) );
} else if( data.label[i] instanceof Element ) {
result.appendChild( data.label[i] );
}
}
node.appendChild( result );
}
break;
case 'submit':
node = document.createElement( 'span' );
childContainder = node.appendChild(document.createElement( 'input' ));
childContainder.setAttribute( 'type', 'submit' );
if( data.label ) {
childContainder.setAttribute( 'value', data.label );
}
childContainder.setAttribute( 'name', data.name || 'submit' );
if( data.disabled ) {
childContainder.setAttribute( 'disabled', 'disabled' );
}
break;
case 'button':
node = document.createElement( 'span' );
childContainder = node.appendChild(document.createElement( 'input' ));
childContainder.setAttribute( 'type', 'button' );
if( data.label ) {
childContainder.setAttribute( 'value', data.label );
}
childContainder.setAttribute( 'name', data.name );
if( data.disabled ) {
childContainder.setAttribute( 'disabled', 'disabled' );
}
if( data.event ) {
childContainder.addEventListener( 'click', data.event, false );
}
break;
case 'textarea':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'h5' ) );
label.appendChild( document.createTextNode( data.label ) );
// TODO need to nest a <label> tag in here without creating extra vertical space
//label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'textarea' ) );
subnode.setAttribute( 'name', data.name );
if( data.cols ) {
subnode.setAttribute( 'cols', data.cols );
}
if( data.rows ) {
subnode.setAttribute( 'rows', data.rows );
}
if( data.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.readonly ) {
subnode.setAttribute( 'readonly', 'readonly' );
}
if( data.value ) {
subnode.value = data.value;
}
break;
default:
throw new Error("Morebits.quickForm: unknown element type " + data.type.toString());
}
if( !childContainder ) {
childContainder = node;
}
if( data.tooltip ) {
Morebits.quickForm.element.generateTooltip( label || node , data );
}
if( data.extra ) {
childContainder.extra = data.extra;
}
if( data.style ) {
childContainder.setAttribute( 'style', data.style );
}
childContainder.setAttribute( 'id', data.id || id );
return [ node, childContainder ];
};
Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip( node, data ) {
$('<span/>', {
'class': 'ui-icon ui-icon-help ui-icon-inline morebits-tooltip'
}).appendTo(node).tipsy({
'fallback': data.tooltip,
'fade': true,
'gravity': $.fn.tipsy.autoWE,
'html': true,
'delayOut': 250
});
};
/**
* Some utility methods for manipulating quickForms after their creation
* (None of them work for "dyninput" type fields at present)
*
* Morebits.quickForm.getElements(form, fieldName)
* Returns all form elements with a given field name or ID
*
* Morebits.quickForm.getCheckboxOrRadio(elementArray, value)
* Searches the array of elements for a checkbox or radio button with a certain |value| attribute
*
* Morebits.quickForm.getElementContainer(element)
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios
*
* Morebits.quickForm.getElementLabelObject(element)
* Gets the HTML element that contains the label of the given form element (mainly for internal use)
*
* Morebits.quickForm.getElementLabel(element)
* Gets the label text of the element
*
* Morebits.quickForm.setElementLabel(element, labelText)
* Sets the label of the element to the given text
*
* Morebits.quickForm.overrideElementLabel(element, temporaryLabelText)
* Stores the element's current label, and temporarily sets the label to the given text
*
* Morebits.quickForm.resetElementLabel(element)
* Restores the label stored by overrideElementLabel
*
* Morebits.quickForm.setElementVisibility(element, visibility)
* Shows or hides a form element plus its label and tooltip
*
* Morebits.quickForm.setElementTooltipVisibility(element, visibility)
* Shows or hides the "question mark" icon next to a form element
*/
Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) {
var $form = $(form);
var $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
if ($elements.length > 0) {
return $elements.toArray();
}
return null;
};
Morebits.quickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
var found = $.grep(elementArray, function(el) {
return el.value === value;
});
if (found.length > 0) {
return found[0];
}
return null;
};
Morebits.quickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
Morebits.quickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === "button" || element.type === "submit" ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName("legend")[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName("h5")[0];
// for others, the label is the sibling <label> element
} else {
return element.parentNode.getElementsByTagName("label")[0];
}
return null;
};
Morebits.quickForm.getElementLabel = function QuickFormGetElementLabel(element) {
var labelElement = Morebits.quickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
var labelElement = Morebits.quickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute("data-oldlabel")) {
element.setAttribute("data-oldlabel", Morebits.quickForm.getElementLabel(element));
}
return Morebits.quickForm.setElementLabel(element, temporaryLabelText);
};
Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute("data-oldlabel")) {
return Morebits.quickForm.setElementLabel(element, element.getAttribute("data-oldlabel"));
}
return null;
};
Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.quickForm.getElementContainer(element)).find(".morebits-tooltip").toggle(visibility);
};
/**
* **************** HTMLFormElement ****************
*
* getChecked:
* XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better
*
* Returns an array containing the values of elements with the given name, that has it's
* checked property set to true. (i.e. a checkbox or a radiobutton is checked), or select options
* that have selected set to true. (don't try to mix selects with radio/checkboxes, please)
* Type is optional and can specify if either radio or checkbox (for the event
* that both checkboxes and radiobuttons have the same name.
*/
HTMLFormElement.prototype.getChecked = function( name, type ) {
var elements = this.elements[name];
if( !elements ) {
// if the element doesn't exists, return null.
return null;
}
var return_array = [];
var i;
if( elements instanceof HTMLSelectElement ) {
var options = elements.options;
for( i = 0; i < options.length; ++i ) {
if( options[i].selected ) {
if( options[i].values ) {
return_array.push( options[i].values );
} else {
return_array.push( options[i].value );
}
}
}
} else if( elements instanceof HTMLInputElement ) {
if( type && elements.type !== type ) {
return [];
} else if( elements.checked ) {
return [ elements.value ];
}
} else {
for( i = 0; i < elements.length; ++i ) {
if( elements[i].checked ) {
if( type && elements[i].type !== type ) {
continue;
}
if( elements[i].values ) {
return_array.push( elements[i].values );
} else {
return_array.push( elements[i].value );
}
}
}
}
return return_array;
};
/**
* **************** RegExp ****************
*
* RegExp.escape: Will escape a string to be used in a RegExp
*/
RegExp.escape = function( text, space_fix ) {
text = mw.RegExp.escape(text);
// Special MediaWiki escape - underscore/space are often equivalent
if( space_fix ) {
text = text.replace( / |_/g, '[_ ]' );
}
return text;
};
/**
* **************** Morebits.bytes ****************
* Utility object for formatting byte values
*/
Morebits.bytes = function( value ) {
if( typeof value === 'string' ) {
var res = /(\d+) ?(\w?)(i?)B?/.exec( value );
var number = res[1];
var mag = res[2];
var si = res[3];
if( !number ) {
this.number = 0;
return;
}
if( !si ) {
this.value = number * Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 );
} else {
this.value = number * Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 );
}
} else {
this.value = value;
}
};
Morebits.bytes.magnitudes = {
'': 0,
'K': 1,
'M': 2,
'G': 3,
'T': 4,
'P': 5,
'E': 6,
'Z': 7,
'Y': 8
};
Morebits.bytes.rmagnitudes = {
0: '',
1: 'K',
2: 'M',
3: 'G',
4: 'T',
5: 'P',
6: 'E',
7: 'Z',
8: 'Y'
};
Morebits.bytes.prototype.valueOf = function() {
return this.value;
};
Morebits.bytes.prototype.toString = function( magnitude ) {
var tmp = this.value;
if( magnitude ) {
var si = /i/.test(magnitude);
var mag = magnitude.replace( /.*?(\w)i?B?.*/g, '$1' );
if( si ) {
tmp /= Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 );
} else {
tmp /= Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 );
}
if( parseInt( tmp, 10 ) !== tmp ) {
tmp = Number( tmp ).toPrecision( 4 );
}
return tmp + ' ' + mag + (si?'i':'') + 'B';
} else {
// si per default
var current = 0;
while( tmp >= 1024 ) {
tmp /= 1024;
++current;
}
tmp = this.value / Math.pow( 2, current * 10 );
if( parseInt( tmp, 10 ) !== tmp ) {
tmp = Number( tmp ).toPrecision( 4 );
}
return tmp + ' ' + Morebits.bytes.rmagnitudes[current] + ( current > 0 ? 'iB' : 'B' );
}
};
/**
* **************** String; Morebits.string ****************
*/
if (!String.prototype.trimLeft) {
String.prototype.trimLeft = function stringPrototypeLtrim( chars ) {
chars = chars || "\\s";
return this.replace( new RegExp("^[" + chars + "]+", "g"), "" );
};
}
if (!String.prototype.trimRight) {
String.prototype.trimRight = function stringPrototypeRtrim( chars ) {
chars = chars || "\\s";
return this.replace( new RegExp("[" + chars + "]+$", "g"), "" );
};
}
if (!String.prototype.trim) {
String.prototype.trim = function stringPrototypeTrim( chars ) {
return this.trimRight(chars).trimLeft(chars);
};
}
// Helper functions to change case of a string
Morebits.string = {
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 );
},
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.substr( 0, 1 ).toLowerCase() + str.substr( 1 );
},
splitWeightedByKeys: function( str, start, end, skip ) {
if( start.length !== end.length ) {
throw new Error( 'start marker and end marker must be of the same length' );
}
var level = 0;
var initial = null;
var result = [];
if( ! $.isArray( skip ) ) {
if( skip === undefined ) {
skip = [];
} else if( typeof skip === 'string' ) {
skip = [ skip ];
} else {
throw new Error( "non-applicable skip parameter" );
}
}
for( var i = 0; i < str.length; ++i ) {
for( var j = 0; j < skip.length; ++j ) {
if( str.substr( i, skip[j].length ) === skip[j] ) {
i += skip[j].length - 1;
continue;
}
}
if( str.substr( i, start.length ) === start ) {
if( initial === null ) {
initial = i;
}
++level;
i += start.length - 1;
} else if( str.substr( i, end.length ) === end ) {
--level;
i += end.length - 1;
}
if( !level && initial !== null ) {
result.push( str.substring( initial, i + 1 ) );
initial = null;
}
}
return result;
}
};
/**
* **************** Morebits.array ****************
*
* uniq(arr): returns a copy of the array with duplicates removed
*
* dups(arr): returns a copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain
*
* chunk(arr, size): breaks up |arr| into smaller arrays of length |size|, and
* returns an array of these "chunked" arrays
*/
Morebits.array = {
uniq: function(arr) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.uniq";
}
var result = [];
for( var i = 0; i < arr.length; ++i ) {
var current = arr[i];
if( result.indexOf( current ) === -1 ) {
result.push( current );
}
}
return result;
},
dups: function(arr) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.dups";
}
var uniques = [];
var result = [];
for( var i = 0; i < arr.length; ++i ) {
var current = arr[i];
if( uniques.indexOf( current ) === -1 ) {
uniques.push( current );
} else {
result.push( current );
}
}
return result;
},
chunk: function( arr, size ) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.chunk";
}
if( typeof size !== 'number' || size <= 0 ) { // pretty impossible to do anything :)
return [ arr ]; // we return an array consisting of this array.
}
var result = [];
var current;
for( var i = 0; i < arr.length; ++i ) {
if( i % size === 0 ) { // when 'i' is 0, this is always true, so we start by creating one.
current = [];
result.push( current );
}
current.push( arr[i] );
}
return result;
}
};
/**
* **************** Morebits.getPageAssociatedUser ****************
* Get the user associated with the currently-viewed page.
* Currently works on User:, User talk:, Special:Contributions.
*/
Morebits.getPageAssociatedUser = function(){
var thisNamespaceId = mw.config.get('wgNamespaceNumber');
if ( thisNamespaceId === 2 /* User: */ || thisNamespaceId === 3 /* User talk: */ ) {
return mw.config.get('wgTitle').split( '/' )[0]; // only first part before any slashes, to work on subpages
}
if ( thisNamespaceId === -1 /* Special: */ && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) {
return mw.config.get("wgRelevantUserName");
}
return false;
};
/**
* **************** Morebits.unbinder ****************
* Used by Morebits.wikitext.page.commentOutImage
*/
Morebits.unbinder = function Unbinder( string ) {
if( typeof string !== 'string' ) {
throw new Error( "not a string" );
}
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
}
Morebits.unbinder.prototype = {
unbind: function UnbinderUnbind( prefix, postfix ) {
var re = new RegExp( prefix + '(.*?)' + postfix, 'g' );
this.content = this.content.replace( re, Morebits.unbinder.getCallback( this ) );
},
rebind: function UnbinderRebind() {
var content = this.content;
content.self = this;
for( var current in this.history ) {
if( this.history.hasOwnProperty( current ) ) {
content = content.replace( current, this.history[current] );
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback( match , a , b ) {
var current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/**
* **************** Date ****************
* Helper functions to get the month as a string instead of a number
*
* Normally it is poor form to play with prototypes of primitive types, but it
* is fairly unlikely that anyone will iterate over a Date object.
*/
Date.monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
Date.monthNamesAbbrev = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
Date.prototype.getMonthName = function() {
return Date.monthNames[ this.getMonth() ];
};
Date.prototype.getMonthNameAbbrev = function() {
return Date.monthNamesAbbrev[ this.getMonth() ];
};
Date.prototype.getUTCMonthName = function() {
return Date.monthNames[ this.getUTCMonth() ];
};
Date.prototype.getUTCMonthNameAbbrev = function() {
return Date.monthNamesAbbrev[ this.getUTCMonth() ];
};
/**
* **************** Morebits.wikipedia ****************
* English Wikipedia-specific objects
*/
Morebits.wikipedia = {};
Morebits.wikipedia.namespaces = {
'-2': 'Media',
'-1': 'Special',
'0': '',
'1': 'Talk',
'2': 'User',
'3': 'User talk',
'4': 'Project',
'5': 'Project talk',
'6': 'File',
'7': 'File talk',
'8': 'MediaWiki',
'9': 'MediaWiki talk',
'10': 'Template',
'11': 'Template talk',
'12': 'Help',
'13': 'Help talk',
'14': 'Category',
'15': 'Category talk',
'100': 'Portal',
'101': 'Portal talk',
'108': 'Book',
'109': 'Book talk'
};
Morebits.wikipedia.namespacesFriendly = {
'0': '(Article)',
'1': 'Talk',
'2': 'User',
'3': 'User talk',
'4': 'Wikipedia',
'5': 'Wikipedia talk',
'6': 'File',
'7': 'File talk',
'8': 'MediaWiki',
'9': 'MediaWiki talk',
'10': 'Template',
'11': 'Template talk',
'12': 'Help',
'13': 'Help talk',
'14': 'Category',
'15': 'Category talk',
'100': 'Portal',
'101': 'Portal talk',
'108': 'Book',
'109': 'Book talk'
};
/**
* **************** Morebits.wiki ****************
* Various objects for wiki editing and API access
*/
Morebits.wiki = {};
// Analyzes the HTML of the current page (i.e. no AJAX requests) to determine if it
// is a redirect or soft redirect
Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() {
return !!($("span.redirectText").length > 0 || document.getElementById("softredirect"));
};
/**
* **************** Morebits.wiki.actionCompleted ****************
*
* Use of Morebits.wiki.actionCompleted():
* Every call to Morebits.wiki.api.post() results in the dispatch of
* an asynchronous callback. Each callback can in turn
* make an additional call to Morebits.wiki.api.post() to continue a
* processing sequence. At the conclusion of the final callback
* of a processing sequence, it is not possible to simply return to the
* original caller because there is no call stack leading back to
* the original context. Instead, Morebits.wiki.actionCompleted.event() is
* called to display the result to the user and to perform an optional
* page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event()
* is managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.api call and decremented
* after the completion of a callback function. If a callback function
* does not create a new Morebits.wiki.api object before exiting, it is the
* final step in the processing chain and Morebits.wiki.actionCompleted.event()
* will then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback function.
* This is used for batch operations. The end of a batch is signaled by calling
* Morebits.wiki.removeCheckpoint().
*/
Morebits.wiki.numberOfActionsLeft = 0;
Morebits.wiki.nbrOfCheckpointsLeft = 0;
Morebits.wiki.actionCompleted = function( self ) {
if( --Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0 ) {
Morebits.wiki.actionCompleted.event( self );
}
};
// Change per action wanted
Morebits.wiki.actionCompleted.event = function() {
new Morebits.status( Morebits.wiki.actionCompleted.notice, Morebits.wiki.actionCompleted.postfix, 'info' );
if( Morebits.wiki.actionCompleted.redirect ) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if( !( (/^\w+\:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl( Morebits.wiki.actionCompleted.redirect );
if( Morebits.wiki.actionCompleted.followRedirect === false ) {
Morebits.wiki.actionCompleted.redirect += "?redirect=no";
}
}
window.setTimeout( function() { window.location = Morebits.wiki.actionCompleted.redirect; }, Morebits.wiki.actionCompleted.timeOut );
}
};
Morebits.wiki.actionCompleted.timeOut = ( typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut );
Morebits.wiki.actionCompleted.redirect = null;
Morebits.wiki.actionCompleted.notice = 'Action';
Morebits.wiki.actionCompleted.postfix = 'completed';
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
Morebits.wiki.removeCheckpoint = function() {
if( --Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0 ) {
Morebits.wiki.actionCompleted.event();
}
};
/**
* **************** Morebits.wiki.api ****************
* An easy way to talk to the MediaWiki API.
*
* Constructor parameters:
* currentAction: the current action (required)
* query: the query (required)
* onSuccess: the function to call when request gotten
* statusElement: a Morebits.status object to use for status messages (optional)
* onError: the function to call if an error occurs (optional)
*/
Morebits.wiki.api = function( currentAction, query, onSuccess, statusElement, onError ) {
this.currentAction = currentAction;
this.query = query;
this.query.format = 'xml';
this.query.assert = 'user';
this.onSuccess = onSuccess;
this.onError = onError;
if( statusElement ) {
this.statelem = statusElement;
this.statelem.status( currentAction );
} else {
this.statelem = new Morebits.status( currentAction );
}
};
Morebits.wiki.api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
responseXML: null,
setParent: function(parent) { this.parent = parent; }, // keep track of parent object for callbacks
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
// post(): carries out the request
// do not specify a parameter unless you really really want to give jQuery some extra parameters
post: function( callerAjaxParameters ) {
++Morebits.wiki.numberOfActionsLeft;
var ajaxparams = $.extend( {}, {
context: this,
type: 'POST',
url: mw.util.wikiScript('api'),
data: Morebits.queryString.create(this.query),
datatype: 'xml',
headers: {
'Api-User-Agent': morebitsWikiApiUserAgent
},
success: function(xml, statusText, jqXHR) {
this.statusText = statusText;
this.responseXML = xml;
this.errorCode = $(xml).find('error').attr('code');
this.errorText = $(xml).find('error').attr('info');
if (typeof this.errorCode === "string") {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
this.returnError();
return;
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call( this.parent, this );
} else {
this.statelem.info("done");
}
Morebits.wiki.actionCompleted();
},
// only network and server errors reach here – complaints from the API itself are caught in success()
error: function(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
this.returnError();
}
}, callerAjaxParameters );
return $.ajax( ajaxparams ); // the return value should be ignored, unless using callerAjaxParameters with |async: false|
},
returnError: function() {
this.statelem.error( this.errorText );
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call( this.parent, this );
}
// don't complete the action so that the error remains displayed
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() {
return this.responseXML;
}
};
// Custom user agent header, used by WMF for server-side logging
// See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html
var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])';
// Sets the custom user agent header
Morebits.wiki.api.setApiUserAgent = function( ua ) {
morebitsWikiApiUserAgent = ( ua ? ua + ' ' : '' ) + 'morebits.js/2.0 ([[w:WT:TW]])';
};
/**
* **************** Morebits.wiki.page ****************
* Uses the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set Morebits.wiki.actionCompleted.notice and Morebits.wiki.actionCompleted.redirect
* before the first call to Morebits.wiki.page.load().
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.page prototype method using this reference.
*
*
* NOTE: This list of member functions is incomplete.
*
* Constructor: Morebits.wiki.page(pageName, currentAction)
* pageName - the name of the page, prefixed by the namespace (if any)
* (for the current page, use mw.config.get('wgPageName'))
* currentAction - a string describing the action about to be undertaken (optional)
*
* load(onSuccess, onFailure): Loads the text for the page
* onSuccess - callback function which is called when the load has succeeded
* onFailure - callback function which is called when the load fails (optional)
* XXX onFailure for load() is not yet implemented – do we need it? -- UncleDouggie
* probably not -- TTO
*
* save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load().
* onSuccess - callback function which is called when the save has succeeded (optional)
* onFailure - callback function which is called when the save fails (optional)
* Warning: Calling save() can result in additional calls to the previous load() callbacks to
* recover from edit conflicts!
* In this case, callers must make the same edit to the new pageText and reinvoke save().
* This behavior can be disabled with setMaxConflictRetries(0).
*
* append(onSuccess, onFailure): Adds the text provided via setAppendText() to the end of the page.
* Does not require calling load() first.
* onSuccess - callback function which is called when the method has succeeded (optional)
* onFailure - callback function which is called when the method fails (optional)
*
* prepend(onSuccess, onFailure): Adds the text provided via setPrependText() to the start of the page.
* Does not require calling load() first.
* onSuccess - callback function which is called when the method has succeeded (optional)
* onFailure - callback function which is called when the method fails (optional)
*
* getPageName(): returns a string containing the name of the loaded page, including the namespace
*
* getPageText(): returns a string containing the text of the page after a successful load()
*
* setPageText(pageText)
* pageText - string containing the updated page text that will be saved when save() is called
*
* setAppendText(appendText)
* appendText - string containing the text that will be appended to the page when append() is called
*
* setPrependText(prependText)
* prependText - string containing the text that will be prepended to the page when prepend() is called
*
* setEditSummary(summary)
* summary - string containing the text of the edit summary that will be used when save() is called
*
* setMinorEdit(minorEdit)
* minorEdit is a boolean value:
* true - When save is called, the resulting edit will be marked as "minor".
* false - When save is called, the resulting edit will not be marked as "minor". (default)
*
* setPageSection(pageSection)
* pageSection - integer specifying the section number to load or save. The default is |null|, which means
* that the entire page will be retrieved.
*
* setMaxConflictRetries(maxRetries)
* maxRetries - number of retries for save errors involving an edit conflict or loss of edit token
* default: 2
*
* setMaxRetries(maxRetries)
* maxRetries - number of retries for save errors not involving an edit conflict or loss of edit token
* default: 2
*
* setCallbackParameters(callbackParameters)
* callbackParameters - an object for use in a callback function
*
* getCallbackParameters(): returns the object previous set by setCallbackParameters()
*
* Callback notes: callbackParameters is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback function.
* Callers must ensure that any changes to the callbackParameters object
* within a load() callback still permit a proper re-entry into the
* load() callback if an edit conflict is detected upon calling save().
*
* getStatusElement(): returns the Status element created by the constructor
*
* setFollowRedirect(followRedirect)
* followRedirect is a boolean value:
* true - a maximum of one redirect will be followed.
* In the event of a redirect, a message is displayed to the user and
* the redirect target can be retrieved with getPageName().
* false - the requested pageName will be used without regard to any redirect. (default)
*
* setWatchlist(watchlistOption)
* watchlistOption is a boolean value:
* true - page will be added to the user's watchlist when save() is called
* false - watchlist status of the page will not be changed (default)
*
* setWatchlistFromPreferences(watchlistOption)
* watchlistOption is a boolean value:
* true - page watchlist status will be set based on the user's
* preference settings when save() is called
* false - watchlist status of the page will not be changed (default)
*
* Watchlist notes:
* 1. The MediaWiki API value of 'unwatch', which explicitly removes the page from the
* user's watchlist, is not used.
* 2. If both setWatchlist() and setWatchlistFromPreferences() are called,
* the last call takes priority.
* 3. Twinkle modules should use the appropriate preference to set the watchlist options.
* 4. Most Twinkle modules use setWatchlist().
* setWatchlistFromPreferences() is only needed for the few Twinkle watchlist preferences
* that accept a string value of 'default'.
*
* setCreateOption(createOption)
* createOption is a string value:
* 'recreate' - create the page if it does not exist, or edit it if it exists
* 'createonly' - create the page if it does not exist, but return an error if it
* already exists
* 'nocreate' - don't create the page, only edit it if it already exists
* null - create the page if it does not exist, unless it was deleted in the moment
* between retrieving the edit token and saving the edit (default)
*
* exists(): returns true if the page existed on the wiki when it was last loaded
*
* lookupCreator(onSuccess): Retrieves the username of the user who created the page
* onSuccess - callback function which is called when the username is found
* within the callback, the username can be retrieved using the getCreator() function
*
* getCreator(): returns the user who created the page following lookupCreator()
*
* patrol(): marks the page as patrolled (only when "rcid" is present in the query string)
*
* move(onSuccess, onFailure): Moves a page to another title
*
* deletePage(onSuccess, onFailure): Deletes a page (for admins only)
*
*/
/**
* Call sequence for common operations (optional final user callbacks not shown):
*
* Edit current contents of a page (no edit conflict):
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Edit current contents of a page (with edit conflict):
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() ->
* ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Append to a page (similar for prepend):
* .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Notes:
* 1. All functions following Morebits.wiki.api.post() are invoked asynchronously
* from the jQuery AJAX library.
* 2. The sequence for append/prepend could be slightly shortened, but it would require
* significant duplication of code for little benefit.
*/
Morebits.wiki.page = function(pageName, currentAction) {
if (!currentAction) {
currentAction = 'Opening page "' + pageName + '"';
}
/**
* Private context variables
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*/
var ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
callbackParameters: null,
statusElement: new Morebits.status(currentAction),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
createOption: null,
minorEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
watchlistOption: 'nochange',
creator: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
editToken: null,
loadTime: null,
lastEditTime: null,
revertCurID: null,
revertUser: null,
fullyProtected: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreatorSuccess: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
saveApi: null,
lookupCreatorApi: null,
moveApi: null,
moveProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
var emptyFunction = function() { };
/**
* Public interface accessors
*/
this.getPageName = function() {
return ctx.pageName;
};
this.getPageText = function() {
return ctx.pageText;
};
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
this.setMaxConflictRetries = function(maxRetries) {
ctx.maxConflictRetries = maxRetries;
};
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
this.getCreator = function() {
return ctx.creator;
};
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
this.getRevisionUser = function() {
return ctx.revertUser;
};
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry };
};
this.getStatusElement = function() {
return ctx.statusElement;
};
this.setFollowRedirect = function(followRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error("Internal error: cannot change redirect setting after the page has been loaded!");
return;
}
ctx.followRedirect = followRedirect;
};
this.setWatchlist = function(flag) {
if (flag) {
ctx.watchlistOption = 'watch';
} else {
ctx.watchlistOption = 'nochange';
}
};
this.setWatchlistFromPreferences = function(flag) {
if (flag) {
ctx.watchlistOption = 'preferences';
} else {
ctx.watchlistOption = 'nochange';
}
};
this.exists = function() {
return ctx.pageExists;
};
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error("Internal error: no onSuccess callback provided to load()!");
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsInGroup('sysop')) {
ctx.loadQuery.inprop = 'protection';
}
ctx.loadApi = new Morebits.wiki.api("Retrieving page...", ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
// Save updated .pageText to Wikipedia
// Only valid after successful .load()
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.pageLoaded) {
ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!");
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: edit summary not set before save!");
ctx.onSaveFailure(this);
return;
}
if (ctx.fullyProtected && !confirm('You are about to make an edit to the fully protected page "' + ctx.pageName +
(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) +
'. \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) {
ctx.statusElement.error("Edit to fully protected page was aborted.");
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
var query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: ctx.editToken,
watchlist: ctx.watchlistOption
};
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
switch (ctx.editMode) {
case 'append':
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default:
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) {
query[ctx.createOption] = '';
}
ctx.saveApi = new Morebits.wiki.api( "Saving page...", query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.lookupCreator = function(onSuccess) {
if (!onSuccess) {
ctx.statusElement.error("Internal error: no onSuccess callback provided to lookupCreator()!");
return;
}
ctx.onLookupCreatorSuccess = onSuccess;
var query = {
'action': 'query',
'prop': 'revisions',
'titles': ctx.pageName,
'rvlimit': 1,
'rvprop': 'user',
'rvdir': 'newer'
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreatorApi = new Morebits.wiki.api("Retrieving page creator information", query, fnLookupCreatorSuccess, ctx.statusElement);
ctx.lookupCreatorApi.setParent(this);
ctx.lookupCreatorApi.post();
};
this.patrol = function() {
// look for rcid in querystring; if not, we won't have a patrol token, so no point trying
if (!Morebits.queryString.exists("rcid")) {
return;
}
var rcid = Morebits.queryString.get("rcid");
// extract patrol token from "Mark page as patrolled" link on page
var patrollinkmatch = /token=(.+)%2B%5C$/.exec($(".patrollink a").attr("href"));
if (patrollinkmatch) {
var patroltoken = patrollinkmatch[1] + "+\\";
var patrolstat = new Morebits.status("Marking page as patrolled");
var wikipedia_api = new Morebits.wiki.api("doing...", {
title: ctx.pageName,
action: 'markpatrolled',
rcid: rcid,
token: patroltoken
}, null, patrolstat);
wikipedia_api.post({
type: 'GET',
url: mw.util.wikiScript('index'),
datatype: 'text' // we don't really care about the response
});
}
};
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error("Internal error: revision ID to revert to was not set before revert!");
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: move reason not set before move (use setEditSummary function)!");
ctx.onMoveFailure(this);
return;
}
if (!ctx.moveDestination) {
ctx.statusElement.error("Internal error: destination page name was not set before move!");
ctx.onMoveFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
if (Morebits.userIsInGroup('sysop')) {
query.inprop = 'protection';
}
ctx.moveApi = new Morebits.wiki.api("retrieving move token...", query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
};
// |delete| is a reserved word in some flavours of JS
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot delete page: only admins can do that");
ctx.onDeleteFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: delete reason not set before delete (use setEditSummary function)!");
ctx.onDeleteFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
inprop: 'protection',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
};
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot protect page: only admins can do that");
ctx.onProtectFailure(this);
return;
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error("Internal error: you must set edit and/or move and/or create protection before calling protect()!");
ctx.onProtectFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: protection reason not set before protect (use setEditSummary function)!");
ctx.onProtectFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
inprop: 'protection',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.protectApi = new Morebits.wiki.api("retrieving protect token...", query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
// apply FlaggedRevs protection-style settings
// only works where $wgFlaggedRevsProtection = true (i.e. where FlaggedRevs
// settings appear on the wiki's "protect" tab)
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot apply FlaggedRevs settings: only admins can do that");
ctx.onStabilizeFailure(this);
return;
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error("Internal error: you must set flaggedRevs before calling stabilize()!");
ctx.onStabilizeFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: reason not set before calling stabilize() (use setEditSummary function)!");
ctx.onStabilizeFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info|flagged',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.stabilizeApi = new Morebits.wiki.api("retrieving stabilize token...", query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
};
/**
* Private member functions
*
* These are not exposed outside
*/
// callback from loadSuccess() for append() and prepend() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
var xml = ctx.loadApi.getXML();
if ( !fnCheckPageName(xml, ctx.onLoadFailure) ) {
return; // abort
}
ctx.pageExists = ($(xml).find('page').attr('missing') !== "");
if (ctx.pageExists) {
ctx.pageText = $(xml).find('rev').text();
} else {
ctx.pageText = ''; // allow for concatenation, etc.
}
// extract protection info, to alert admins when they are about to edit a protected page
if (Morebits.userIsInGroup('sysop')) {
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop') {
ctx.fullyProtected = editprot.attr('expiry');
} else {
ctx.fullyProtected = false;
}
}
ctx.editToken = $(xml).find('tokens').attr('csrftoken');
if (!ctx.editToken)
{
ctx.statusElement.error("Failed to retrieve edit token.");
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = $(xml).find('api').attr('curtimestamp');
if (!ctx.loadTime)
{
ctx.statusElement.error("Failed to retrieve start timestamp.");
ctx.onLoadFailure(this);
return;
}
ctx.lastEditTime = $(xml).find('page').attr('touched');
if (ctx.editMode === 'revert') {
ctx.revertCurID = $(xml).find('rev').attr('revid');
if (!ctx.revertCurID) {
ctx.statusElement.error("Failed to retrieve current revision ID.");
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = $(xml).find('rev').attr('user');
if (!ctx.revertUser) {
if ($(xml).find('rev').attr('userhidden') === "") { // username was RevDel'd or oversighted
ctx.revertUser = "<username hidden>";
} else {
ctx.statusElement.error("Failed to retrieve user who made the revision.");
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = "[[Help:Revert|Reverted]] to revision " + ctx.revertOldID + " by " + ctx.revertUser + ": " + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(xml, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
// check for invalid titles
if ( $(xml).find('page').attr('invalid') === "" ) {
ctx.statusElement.error("The page title is invalid: " + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
if ( $(xml).find('page').attr('title') ) {
var resolvedName = $(xml).find('page').attr('title');
// only notify user for redirects, not normalization
if ( $(xml).find('redirects').length > 0 ) {
Morebits.status.info("Info", "Redirected from " + ctx.pageName + " to " + resolvedName );
}
ctx.pageName = resolvedName; // always update in case of normalization
}
else {
// could be a circular redirect or other problem
ctx.statusElement.error("Could not resolve redirects for: " + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/revert modes
var xml = ctx.saveApi.getXML();
// see if the API thinks we were successful
if ($(xml).find('edit').attr('result') === "Success") {
// real success
// default on success action - display link for edited page
var link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName) );
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['completed (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki
// Wikimedia wikis should only return spam blacklist errors and captchas
var blacklist = $(xml).find('edit').attr('spamblacklist');
if (blacklist) {
var code = document.createElement('code');
code.style.fontFamily = "monospace";
code.appendChild(document.createTextNode(blacklist));
ctx.statusElement.error(['Could not save the page because the URL ', code, ' is on the spam blacklist.']);
}
else if ( $(xml).find('captcha').length > 0 ) {
ctx.statusElement.error("Could not save the page because the wiki server wanted you to fill out a CAPTCHA.");
}
else {
ctx.statusElement.error("Unknown error received from API while saving page");
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
var errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if ( errorCode === "editconflict" && ctx.conflictRetries++ < ctx.maxConflictRetries ) {
// edit conflicts can occur when the page needs to be purged from the server cache
var purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement);
var result = purgeApi.post( { async: false } ); // just wait for it, result is for debugging
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info("Edit conflict detected, reapplying edit");
ctx.loadApi.post(); // reload the page and reapply the edit
// check for loss of edit token
// it's impractical to request a new token here, so invoke edit conflict logic when this happens
} else if ( errorCode === "notoken" && ctx.conflictRetries++ < ctx.maxConflictRetries ) {
ctx.statusElement.info("Edit token is invalid, retrying");
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.loadApi.post(); // reload
// check for network or server error
} else if ( errorCode === "undefined" && ctx.retries++ < ctx.maxRetries ) {
// the error might be transient, so try again
ctx.statusElement.info("Save failed, retrying");
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.saveApi.post(); // give it another go!
// hard error, give up
} else {
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
if ( errorCode === "protectedpage" ) {
ctx.statusElement.error( "Failed to save edit: Page is fully protected" );
} else {
ctx.statusElement.error( "Failed to save edit: " + ctx.saveApi.getErrorText() );
}
ctx.editMode = 'all'; // cancel append/prepend/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
var fnLookupCreatorSuccess = function() {
var xml = ctx.lookupCreatorApi.getXML();
if ( !fnCheckPageName(xml) ) {
return; // abort
}
ctx.creator = $(xml).find('rev').attr('user');
if (!ctx.creator) {
ctx.statusElement.error("Could not find name of page creator");
return;
}
ctx.onLookupCreatorSuccess(this);
};
var fnProcessMove = function() {
var xml = ctx.moveApi.getXML();
if ($(xml).find('page').attr('missing') === "") {
ctx.statusElement.error("Cannot move the page, because it no longer exists");
ctx.onMoveFailure(this);
return;
}
// extract protection info
if (Morebits.userIsInGroup('sysop')) {
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to move the fully protected page "' + ctx.pageName +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
'. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) {
ctx.statusElement.error("Move of fully protected page was aborted.");
ctx.onMoveFailure(this);
return;
}
}
var moveToken = $(xml).find('tokens').attr('csrftoken');
if (!moveToken) {
ctx.statusElement.error("Failed to retrieve move token.");
ctx.onMoveFailure(this);
return;
}
var query = {
'action': 'move',
'from': $(xml).find('page').attr('title'),
'to': ctx.moveDestination,
'token': moveToken,
'reason': ctx.editSummary
};
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true'; // XXX don't know whether this works for non-admins
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.api("moving page...", query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessDelete = function() {
var xml = ctx.deleteApi.getXML();
if ($(xml).find('page').attr('missing') === "") {
ctx.statusElement.error("Cannot delete the page, because it no longer exists");
ctx.onDeleteFailure(this);
return;
}
// extract protection info
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to delete the fully protected page "' + ctx.pageName +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
'. \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) {
ctx.statusElement.error("Deletion of fully protected page was aborted.");
ctx.onDeleteFailure(this);
return;
}
var deleteToken = $(xml).find('tokens').attr('csrftoken');
if (!deleteToken) {
ctx.statusElement.error("Failed to retrieve delete token.");
ctx.onDeleteFailure(this);
return;
}
var query = {
'action': 'delete',
'title': $(xml).find('page').attr('title'),
'token': deleteToken,
'reason': ctx.editSummary
};
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
var fnProcessProtect = function() {
var xml = ctx.protectApi.getXML();
var missing = ($(xml).find('page').attr('missing') === "");
if (((ctx.protectEdit || ctx.protectMove) && missing)) {
ctx.statusElement.error("Cannot protect the page, because it no longer exists");
ctx.onProtectFailure(this);
return;
}
if (ctx.protectCreate && !missing) {
ctx.statusElement.error("Cannot create protect the page, because it already exists");
ctx.onProtectFailure(this);
return;
}
// TODO cascading protection not possible on edit<sysop
var protectToken = $(xml).find('tokens').attr('csrftoken');
if (!protectToken) {
ctx.statusElement.error("Failed to retrieve protect token.");
ctx.onProtectFailure(this);
return;
}
// fetch existing protection levels
var prs = $(xml).find('pr');
var editprot = prs.filter('[type="edit"]');
var moveprot = prs.filter('[type="move"]');
var createprot = prs.filter('[type="create"]');
var protections = [], expirys = [];
// set edit protection level
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
} else if (editprot.length) {
protections.push('edit=' + editprot.attr("level"));
expirys.push(editprot.attr("expiry").replace("infinity", "indefinite"));
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
} else if (moveprot.length) {
protections.push('move=' + moveprot.attr("level"));
expirys.push(moveprot.attr("expiry").replace("infinity", "indefinite"));
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
} else if (createprot.length) {
protections.push('create=' + createprot.attr("level"));
expirys.push(createprot.attr("expiry").replace("infinity", "indefinite"));
}
var query = {
action: 'protect',
title: $(xml).find('page').attr('title'),
token: protectToken,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary
};
if (ctx.protectCascade) {
query.cascade = 'true';
}
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.api("protecting page...", query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
var xml = ctx.stabilizeApi.getXML();
var missing = ($(xml).find('page').attr('missing') === "");
if (missing) {
ctx.statusElement.error("Cannot protect the page, because it no longer exists");
ctx.onStabilizeFailure(this);
return;
}
var stabilizeToken = $(xml).find('tokens').attr('csrftoken');
if (!stabilizeToken) {
ctx.statusElement.error("Failed to retrieve stabilize token.");
ctx.onStabilizeFailure(this);
return;
}
var query = {
action: 'stabilize',
title: $(xml).find('page').attr('title'),
token: stabilizeToken,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
reason: ctx.editSummary
};
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.stabilizeProcessApi = new Morebits.wiki.api("configuring stabilization settings...", query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
}; // end Morebits.wiki.page
/** Morebits.wiki.page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/**
* **************** Morebits.wiki.preview ****************
* Uses the API to parse a fragment of wikitext and render it as HTML.
*
* Constructor: Morebits.wiki.preview(previewbox, currentAction)
* previewbox - the <div> element that will contain the rendered HTML
*
* beginRender(wikitext): Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
* wikitext - wikitext to render; most things should work, including subst: and ~~~~
*
* closePreview(): Hides the preview box and clears it.
*
* The suggested implementation pattern (in Morebits.simpleWindow + Morebits.quickForm situations) is to
* construct a Morebits.wiki.preview object after rendering a Morebits.quickForm, and bind the object
* to an arbitrary property of the form (e.g. |previewer|). For an example, see
* twinklewarn.js.
*/
Morebits.wiki.preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass("morebits-previewbox").hide();
this.beginRender = function(wikitext) {
$(previewbox).show();
var statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.status.init(statusspan);
var query = {
action: 'parse',
prop: 'text',
pst: 'true', // PST = pre-save transform; this makes substitution work properly
text: wikitext,
title: mw.config.get('wgPageName')
};
var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview"));
renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
var xml = apiobj.getXML();
var html = $(xml).find('text').text();
if (!html) {
apiobj.statelem.error("failed to retrieve preview, or template was blanked");
return;
}
previewbox.innerHTML = html;
};
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/**
* **************** Morebits.wikitext ****************
* Wikitext manipulation
*/
Morebits.wikitext = {};
Morebits.wikitext.template = {
parse: function( text, start ) {
var count = -1;
var level = -1;
var equals = -1;
var current = '';
var result = {
name: '',
parameters: {}
};
var key, value;
for( var i = start; i < text.length; ++i ) {
var test3 = text.substr( i, 3 );
if( test3 === '{{{' ) {
current += '{{{';
i += 2;
++level;
continue;
}
if( test3 === '}}}' ) {
current += '}}}';
i += 2;
--level;
continue;
}
var test2 = text.substr( i, 2 );
if( test2 === '{{' || test2 === '[[' ) {
current += test2;
++i;
++level;
continue;
}
if( test2 === '[[' ) {
current += test2;
++i;
--level;
continue;
}
if( test2 === '}}' ) {
current += test2;
++i;
--level;
if( level <= 0 ) {
if( count === -1 ) {
result.name = current.substring(2).trim();
++count;
} else {
if( equals !== -1 ) {
key = current.substring( 0, equals ).trim();
value = current.substring( equals ).trim();
result.parameters[key] = value;
equals = -1;
} else {
result.parameters[count] = current;
++count;
}
}
break;
}
continue;
}
if( text.charAt(i) === '|' && level <= 0 ) {
if( count === -1 ) {
result.name = current.substring(2).trim();
++count;
} else {
if( equals !== -1 ) {
key = current.substring( 0, equals ).trim();
value = current.substring( equals + 1 ).trim();
result.parameters[key] = value;
equals = -1;
} else {
result.parameters[count] = current;
++count;
}
}
current = '';
} else if( equals === -1 && text.charAt(i) === '=' && level <= 0 ) {
equals = current.length;
current += text.charAt(i);
} else {
current += text.charAt(i);
}
}
return result;
}
};
Morebits.wikitext.page = function mediawikiPage( text ) {
this.text = text;
};
Morebits.wikitext.page.prototype = {
text: '',
removeLink: function( link_target ) {
var first_char = link_target.substr( 0, 1 );
var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true );
var link_simple_re = new RegExp( "\\[\\[:?(" + link_re_string + ")\\]\\]", 'g' );
var link_named_re = new RegExp( "\\[\\[:?" + link_re_string + "\\|(.+?)\\]\\]", 'g' );
this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" );
},
commentOutImage: function( image, reason ) {
var unbinder = new Morebits.unbinder( this.text );
unbinder.unbind( '<!--', '-->' );
reason = reason ? (reason + ': ') : '';
var first_char = image.substr( 0, 1 );
var image_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( image.substr( 1 ), true );
/*
* Check for normal image links, i.e. [[Image:Foobar.png|...]]
* Will eat the whole link
*/
var links_re = new RegExp( "\\[\\[(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string );
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( unbinder.content, '[[', ']]' ));
for( var i = 0; i < allLinks.length; ++i ) {
if( links_re.test( allLinks[i] ) ) {
var replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace( allLinks[i], replacement, 'g' );
}
}
// unbind the newly created comments
unbinder.unbind( '<!--', '-->' );
/*
* Check for gallery images, i.e. instances that must start on a new line, eventually preceded with some space, and must include Image: prefix
* Will eat the whole line.
*/
var gallery_image_re = new RegExp( "(^\\s*(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string + ".*?$)", 'mg' );
unbinder.content.replace( gallery_image_re, "<!-- " + reason + "$1 -->" );
// unbind the newly created comments
unbinder.unbind( '<!--', '-->' );
/*
* Check free image usages, for example as template arguments, might have the Image: prefix excluded, but must be preceeded by an |
* Will only eat the image name and the preceeding bar and an eventual named parameter
*/
var free_image_re = new RegExp( "(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile):\\s*)?" + image_re_string + ")", 'mg' );
unbinder.content.replace( free_image_re, "<!-- " + reason + "$1 -->" );
// Rebind the content now, we are done!
this.text = unbinder.rebind();
},
addToImageComment: function( image, data ) {
var first_char = image.substr( 0, 1 );
var first_char_regex = RegExp.escape( first_char, true );
if( first_char.toUpperCase() !== first_char.toLowerCase() ) {
first_char_regex = '[' + RegExp.escape( first_char.toUpperCase(), true ) + RegExp.escape( first_char.toLowerCase(), true ) + ']';
}
var image_re_string = "(?:[Ii]mage|[Ff]ile):\\s*" + first_char_regex + RegExp.escape( image.substr( 1 ), true );
var links_re = new RegExp( "\\[\\[" + image_re_string );
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '[[', ']]' ));
for( var i = 0; i < allLinks.length; ++i ) {
if( links_re.test( allLinks[i] ) ) {
var replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace( /\]\]$/, '|' + data + ']]' );
this.text = this.text.replace( allLinks[i], replacement, 'g' );
}
}
var gallery_re = new RegExp( "^(\\s*" + image_re_string + '.*?)\\|?(.*?)$', 'mg' );
var newtext = "$1|$2 " + data;
this.text = this.text.replace( gallery_re, newtext );
},
removeTemplate: function( template ) {
var first_char = template.substr( 0, 1 );
var template_re_string = "(?:[Tt]emplate:)?\\s*[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( template.substr( 1 ), true );
var links_re = new RegExp( "\\{\\{" + template_re_string );
var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '{{', '}}', [ '{{{', '}}}' ] ));
for( var i = 0; i < allTemplates.length; ++i ) {
if( links_re.test( allTemplates[i] ) ) {
this.text = this.text.replace( allTemplates[i], '', 'g' );
}
}
},
getText: function() {
return this.text;
}
};
/**
* **************** Morebits.queryString ****************
* Maps the querystring to an object
*
* Functions:
*
* Morebits.queryString.exists(key)
* returns true if the particular key is set
* Morebits.queryString.get(key)
* returns the value associated to the key
* Morebits.queryString.equals(key, value)
* returns true if the value associated with given key equals given value
* Morebits.queryString.toString()
* returns the query string as a string
* Morebits.queryString.create( hash )
* creates an querystring and encodes strings via encodeURIComponent and joins arrays with |
*
* In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object.
*
* Example:
*
* var value = Morebits.queryString.get('key');
* var obj = new Morebits.queryString('foo=bar&baz=quux');
* value = obj.get('foo');
*/
Morebits.queryString = function QueryString(qString) {
this.string = qString;
this.params = {};
if( !qString.length ) {
return;
}
qString.replace(/\+/, ' ');
var args = qString.split('&');
for( var i = 0; i < args.length; ++i ) {
var pair = args[i].split( '=' );
var key = decodeURIComponent( pair[0] ), value = key;
if( pair.length === 2 ) {
value = decodeURIComponent( pair[1] );
}
this.params[key] = value;
}
};
Morebits.queryString.staticstr = null;
Morebits.queryString.staticInit = function() {
if( !Morebits.queryString.staticstr ) {
Morebits.queryString.staticstr = new Morebits.queryString(location.search.substring(1));
}
};
Morebits.queryString.get = function(key) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.get(key);
};
Morebits.queryString.prototype.get = function(key) {
return this.params[key] ? this.params[key] : null;
};
Morebits.queryString.exists = function(key) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.exists(key);
};
Morebits.queryString.prototype.exists = function(key) {
return this.params[key] ? true : false;
};
Morebits.queryString.equals = function(key, value) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.equals(key, value);
};
Morebits.queryString.prototype.equals = function(key, value) {
return this.params[key] === value ? true : false;
};
Morebits.queryString.toString = function() {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.toString();
};
Morebits.queryString.prototype.toString = function() {
return this.string ? this.string : null;
};
Morebits.queryString.create = function( arr ) {
var resarr = [];
var editToken; // KLUGE: this should always be the last item in the query string (bug TW-B-0013)
for( var i in arr ) {
if( arr[i] === undefined ) {
continue;
}
var res;
if( $.isArray( arr[i] ) ){
var v = [];
for(var j = 0; j < arr[i].length; ++j ) {
v[j] = encodeURIComponent( arr[i][j] );
}
res = v.join('|');
} else {
res = encodeURIComponent( arr[i] );
}
if( i === 'token' ) {
editToken = res;
} else {
resarr.push( encodeURIComponent( i ) + '=' + res );
}
}
if( editToken !== undefined ) {
resarr.push( 'token=' + editToken );
}
return resarr.join('&');
};
Morebits.queryString.prototype.create = Morebits.queryString.create;
/**
* **************** Morebits.status ****************
*/
Morebits.status = function Status( text, stat, type ) {
this.textRaw = text;
this.text = this.codify(text);
this.type = type || 'status';
this.generate();
if( stat ) {
this.update( stat, type );
}
};
Morebits.status.init = function( root ) {
if( !( root instanceof Element ) ) {
throw new Error( 'object not an instance of Element' );
}
while( root.hasChildNodes() ) {
root.removeChild( root.firstChild );
}
Morebits.status.root = root;
Morebits.status.errorEvent = null;
};
Morebits.status.root = null;
Morebits.status.onError = function( handler ) {
if ( $.isFunction( handler ) ) {
Morebits.status.errorEvent = handler;
} else {
throw "Morebits.status.onError: handler is not a function";
}
};
Morebits.status.prototype = {
stat: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
link: function() {
if( ! this.linked && Morebits.status.root ) {
Morebits.status.root.appendChild( this.node );
this.linked = true;
}
},
unlink: function() {
if( this.linked ) {
Morebits.status.root.removeChild( this.node );
this.linked = false;
}
},
codify: function( obj ) {
if ( ! $.isArray( obj ) ) {
obj = [ obj ];
}
var result;
result = document.createDocumentFragment();
for( var i = 0; i < obj.length; ++i ) {
if( typeof obj[i] === 'string' ) {
result.appendChild( document.createTextNode( obj[i] ) );
} else if( obj[i] instanceof Element ) {
result.appendChild( obj[i] );
} // Else cosmic radiation made something shit
}
return result;
},
update: function( status, type ) {
this.stat = this.codify( status );
if( type ) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.status.errorEvent) {
Morebits.status.errorEvent();
}
// also log error messages in the browser console
if (console && console.error) {
console.error(this.textRaw + ": " + status);
}
}
}
this.render();
},
generate: function() {
this.node = document.createElement( 'div' );
this.node.appendChild( document.createElement('span') ).appendChild( this.text );
this.node.appendChild( document.createElement('span') ).appendChild( document.createTextNode( ': ' ) );
this.target = this.node.appendChild( document.createElement( 'span' ) );
this.target.appendChild( document.createTextNode( '' ) ); // dummy node
},
render: function() {
this.node.className = 'tw_status_' + this.type;
while( this.target.hasChildNodes() ) {
this.target.removeChild( this.target.firstChild );
}
this.target.appendChild( this.stat );
this.link();
},
status: function( status ) {
this.update( status, 'status');
},
info: function( status ) {
this.update( status, 'info');
},
warn: function( status ) {
this.update( status, 'warn');
},
error: function( status ) {
this.update( status, 'error');
}
};
Morebits.status.info = function( text, status ) {
return new Morebits.status( text, status, 'info' );
};
Morebits.status.warn = function( text, status ) {
return new Morebits.status( text, status, 'warn' );
};
Morebits.status.error = function( text, status ) {
return new Morebits.status( text, status, 'error' );
};
/**
* **************** Morebits.htmlNode() ****************
* Simple helper function to create a simple node
*/
Morebits.htmlNode = function ( type, content, color ) {
var node = document.createElement( type );
if( color ) {
node.style.color = color;
}
node.appendChild( document.createTextNode( content ) );
return node;
}
/**
* **************** Morebits.simpleWindow ****************
* A simple draggable window
* now a wrapper for jQuery UI's dialog feature
*/
// The height passed in here is the maximum allowable height for the content area.
Morebits.simpleWindow = function SimpleWindow( width, height ) {
var content = document.createElement( 'div' );
this.content = content;
content.className = 'morebits-dialog-content';
this.height = height;
$(this.content).dialog({
autoOpen: false,
buttons: { "Placeholder button": function() {} },
dialogClass: 'morebits-dialog',
width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)),
// give jQuery the given height value (which represents the anticipated height of the dialog) here, so
// it can position the dialog appropriately
// the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared
// to that of the old SimpleWindow
height: height + 20,
close: function(event, ui) {
// dialogs and their content can be destroyed once closed
$(event.target).dialog("destroy").remove();
},
resize: function(event, ui) {
this.style.maxHeight = "";
}
});
var $widget = $(this.content).dialog("widget");
// add background gradient to titlebar
var $titlebar = $widget.find(".ui-dialog-titlebar");
var oldstyle = $titlebar.attr("style");
$titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;');
// delete the placeholder button (it's only there so the buttonpane gets created)
$widget.find("button").each(function(key, value) {
value.parentNode.removeChild(value);
});
// add container for the buttons we add, and the footer links (if any)
var buttonspan = document.createElement("span");
buttonspan.className = "morebits-dialog-buttons";
var linksspan = document.createElement("span");
linksspan.className = "morebits-dialog-footerlinks";
$widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan);
};
Morebits.simpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
// Focuses the dialog. This might work, or on the contrary, it might not.
focus: function(event) {
$(this.content).dialog("moveToTop");
return this;
},
// Closes the dialog. If this is set as an event handler, it will stop the event from doing anything more.
close: function(event) {
if (event) {
event.preventDefault();
}
$(this.content).dialog("close");
return this;
},
// Shows the dialog. Calling display() on a dialog that has previously been closed might work, but it is not guaranteed.
display: function() {
if (this.scriptName) {
var $widget = $(this.content).dialog("widget");
$widget.find(".morebits-dialog-scriptname").remove();
var scriptnamespan = document.createElement("span");
scriptnamespan.className = "morebits-dialog-scriptname";
scriptnamespan.textContent = this.scriptName + " \u00B7 "; // U+00B7 MIDDLE DOT = ·
$widget.find(".ui-dialog-title").prepend(scriptnamespan);
}
var dialog = $(this.content).dialog("open");
if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) { // tie in with NAVPOP
dialog.parent()[0].ranSetupTooltipsAlready = false;
setupTooltips(dialog.parent()[0]);
}
this.setHeight( this.height ); // init height algorithm
return this;
},
// Sets the dialog title.
setTitle: function( title ) {
$(this.content).dialog("option", "title", title);
return this;
},
// Sets the script name, appearing as a prefix to the title to help users determine which
// user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
setScriptName: function( name ) {
this.scriptName = name;
return this;
},
// Sets the dialog width.
setWidth: function( width ) {
$(this.content).dialog("option", "width", width);
return this;
},
// Sets the dialog's maximum height. The dialog will auto-size to fit its contents,
// but the content area will grow no larger than the height given here.
setHeight: function( height ) {
this.height = height;
// from display time onwards, let the browser determine the optimum height, and instead limit the height at the given value
// note that the given height will exclude the approx. 20px that the jQuery UI chrome has in height in addition to the height
// of an equivalent "classic" Morebits.simpleWindow
if (parseInt(getComputedStyle($(this.content).dialog("widget")[0], null).height, 10) > window.innerHeight) {
$(this.content).dialog("option", "height", window.innerHeight - 2).dialog("option", "position", "top");
} else {
$(this.content).dialog("option", "height", "auto");
}
$(this.content).dialog("widget").find(".morebits-dialog-content")[0].style.maxHeight = parseInt(this.height - 30, 10) + "px";
return this;
},
// Sets the content of the dialog to the given element node, usually from rendering a Morebits.quickForm.
// Re-enumerates the footer buttons, but leaves the footer links as they are.
// Be sure to call this at least once before the dialog is displayed...
setContent: function( content ) {
this.purgeContent();
this.addContent( content );
return this;
},
addContent: function( content ) {
this.content.appendChild( content );
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
var thisproxy = this;
$(this.content).find('input[type="submit"], button[type="submit"]').each(function(key, value) {
value.style.display = "none";
var button = document.createElement("button");
button.textContent = (value.hasAttribute("value") ? value.getAttribute("value") : (value.textContent ? value.textContent : "Submit Query"));
// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
button.addEventListener("click", function() { value.click(); }, false);
thisproxy.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
if (this.buttons.length > 0) {
$(this.content).dialog("widget").find(".morebits-dialog-buttons").empty().append(this.buttons)[0].removeAttribute("data-empty");
} else {
$(this.content).dialog("widget").find(".morebits-dialog-buttons")[0].setAttribute("data-empty", "data-empty"); // used by CSS
}
return this;
},
purgeContent: function( content ) {
this.buttons = [];
// delete all buttons in the buttonpane
$(this.content).dialog("widget").find(".morebits-dialog-buttons").empty();
while( this.content.hasChildNodes() ) {
this.content.removeChild( this.content.firstChild );
}
return this;
},
// Adds a link in the bottom-right corner of the dialog.
// This can be used to provide help or policy links.
// For example, Twinkle's CSD module adds a link to the CSD policy page,
// as well as a link to Twinkle's documentation.
addFooterLink: function( text, wikiPage ) {
var $footerlinks = $(this.content).dialog("widget").find(".morebits-dialog-footerlinks");
if (this.hasFooterLinks) {
var bullet = document.createElement("span");
bullet.textContent = " \u2022 "; // U+2022 BULLET
$footerlinks.append(bullet);
}
var link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(wikiPage) );
link.setAttribute("title", wikiPage);
link.setAttribute("target", "_blank");
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
setModality: function( modal ) {
$(this.content).dialog("option", "modal", modal);
return this;
}
};
// Enables or disables all footer buttons on all Morebits.simpleWindows in the current page.
// This should be called with |false| when the button(s) become irrelevant (e.g. just before Morebits.status.init is called).
// This is not an instance method so that consumers don't have to keep a reference to the original
// Morebits.simpleWindow object sitting around somewhere. Anyway, most of the time there will only be one
// Morebits.simpleWindow open, so this shouldn't matter.
Morebits.simpleWindow.setButtonsEnabled = function( enabled ) {
$(".morebits-dialog-buttons button").attr("disabled", !enabled);
};
// Twinkle blacklist was removed per consensus at http://en.wikipedia.org/wiki/Wikipedia:Administrators%27_noticeboard/Archive221#New_Twinkle_blacklist_proposal
} ( window, document, jQuery )); // End wrap with anonymous function
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if ( typeof arguments === "undefined" ) { // typeof is here for a reason...
window.SimpleWindow = Morebits.simpleWindow;
window.QuickForm = Morebits.quickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.status;
window.QueryString = Morebits.queryString;
}
// </nowiki>
// [[Category:Scripts]]
// [[Category:Twinkle]]
be7ce576a8bd7447153c70167bdc568d1cc37459
Template:If empty
10
98
187
2023-03-21T04:15:21Z
dev>Pppery
0
Rv
wikitext
text/x-wiki
<includeonly>{{{{{|safesubst:}}}#if:{{{1|}}}
| {{{1}}}
| {{{{{|safesubst:}}}#if:{{{2|}}}
| {{{2}}}
| {{{{{|safesubst:}}}#if:{{{3|}}}
| {{{3}}}
| {{{{{|safesubst:}}}#if:{{{4|}}}
| {{{4}}}
| {{{{{|safesubst:}}}#if:{{{5|}}}
| {{{5}}}
| {{{{{|safesubst:}}}#if:{{{6|}}}
| {{{6}}}
| {{{{{|safesubst:}}}#if:{{{7|}}}
| {{{7}}}
| {{{{{|safesubst:}}}#if:{{{8|}}}
| {{{8}}}
| {{{{{|safesubst:}}}#if:{{{9|}}}
| {{{9}}}
}}
}}
}}
}}
}}
}}
}}
}}
}}</includeonly><noinclude>
{{Documentation}}
<!-- Add categories and interwikis to the /doc subpage, not here! -->
</noinclude>
eeda2c13231e9a8b44d480e8c429d73652575009
Template:Infobox character
10
16
25
2023-03-21T04:32:31Z
dev>Pppery
0
wikitext
text/x-wiki
{{Infobox
| bodystyle = border-spacing: 2px 5px;
| above = {{If empty |{{{name|}}} |<includeonly>{{PAGENAMEBASE}}</includeonly> }}
| abovestyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} | color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }}
| subheader = {{#if: {{{series|}}}{{{franchise|}}} | {{#if: {{{series|}}} | ''{{{series|}}}'' | {{{franchise|}}} }} character{{#if: {{{multiple|}}} | s }} }}
| image = {{{image|}}}|
| caption = {{{caption|}}}
| headerstyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} |color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }}
| label1 = First appearance
| data1 = {{#if: {{{first|}}} | {{{first|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{first_major|}}} |minor_work={{{first_minor|}}} |issue={{{first_issue|}}} |date={{{first_date|}}} }} }}
| label2 = First game
| data2 = {{{firstgame|}}}
| label3 = Last appearance
| data3 = {{#if: {{{last|}}} | {{{last|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{last_major|}}} |minor_work={{{last_minor|}}} |issue={{{last_issue|}}} |date={{{last_date|}}} }} }}
| label4 = Created by
| data4 = {{{creator|}}}
| label5 = Based on
| data5 = {{{based_on|}}}
| label6 = Adapted by
| data6 = {{{adapted_by|}}}
| label7 = Designed by
| data7 = {{{designer|}}}
| label8 = Portrayed by
| data8 = {{{portrayer|}}}
| label9 = Voiced by
| data9 = {{{voice|}}}
| label10 = Motion capture
| data10 = {{{motion_actor|}}}
| label11 = {{{lbl1|}}}
| data11 = {{{data1|}}}
| label12 = {{{lbl2|}}}
| data12 = {{{data2|}}}
| label13 = {{{lbl3|}}}
| data13 = {{{data3|}}}
| label14 = {{{lbl4|}}}
| data14 = {{{data4|}}}
| label15 = {{{lbl5|}}}
| data15 = {{{data5|}}}
| header20 = {{#if: {{{noinfo|}}} || {{#if: {{{full_name|}}} {{{alias|}}} {{{aliases|}}} {{{nickname|}}} {{{nicknames|}}} {{{race|}}} {{{species|}}} {{{gender|}}} {{{title|}}} {{{occupation|}}} {{{position|}}} {{{class|}}} {{{affiliation|}}} {{{alignment|}}} {{{fighting_style|}}} {{{weapon|}}} {{{family|}}} {{{spouse|}}} {{{spouses|}}} {{{significant_other|}}} {{{significant_others|}}} {{{children|}}} {{{relatives|}}} {{{religion|}}} {{{origin|}}} {{{home|}}} {{{nationality|}}} {{{data21|}}} {{{data22|}}} {{{data23|}}} {{{data24|}}} {{{data25|}}} | {{If empty |{{{info-hdr|}}} |In-universe information }} }} }}
| label21 = Full name
| data21 = {{{full_name|}}}
| label22 = {{#if: {{{alias|}}} | Alias | Aliases }}
| data22 = {{If empty |{{{alias|}}} |{{{aliases|}}} }}
| label23 = {{#if: {{{nickname|}}} | Nickname | Nicknames }}
| data23 = {{If empty |{{{nickname|}}} |{{{nicknames|}}} }}
| label24 = {{#if: {{{race|}}} | Race | Species }}
| data24 = {{If empty |{{{race|}}} |{{{species|}}} }}
| label25 = Gender
| data25 = {{{gender|}}}
| label26 = Title
| data26 = {{{title|}}}
| label27 = {{#if: {{{occupation|}}} | Occupation | {{#if: {{{position|}}} | Position | Class }} }}
| data27 = {{If empty |{{{occupation|}}} |{{{position|}}} |{{{class|}}} }}
| label28 = {{#if: {{{affiliation|}}} | Affiliation | Alignment }}
| data28 = {{If empty |{{{affiliation|}}} |{{{alignment|}}} }}
| label29 = Fighting style
| data29 = {{{fighting_style|}}}
| label30 = Weapon
| data30 = {{{weapon|}}}
| label31 = Family
| data31 = {{{family|}}}
| label32 = {{#if: {{{spouse|}}} | Spouse | Spouses }}
| data32 = {{If empty |{{{spouse|}}} |{{{spouses|}}} }}
| label33 = {{#if: {{{significant_other|}}} | Significant other | Significant others }}
| data33 = {{If empty |{{{significant_other|}}} |{{{significant_others|}}} }}
| label34 = Children
| data34 = {{{children|}}}
| label35 = Relatives
| data35 = {{{relatives|}}}
| label36 = Religion
| data36 = {{{religion|}}}
| label37 = {{#if: {{{origin|}}} | Origin | Home }}
| data37 = {{If empty |{{{origin|}}} |{{{home|}}} }}
| label38 = Nationality
| data38 = {{{nationality|}}}
| label39 = {{{lbl21|}}}
| data39 = {{{data21|}}}
| label40 = {{{lbl22|}}}
| data40 = {{{data22|}}}
| label41 = {{{lbl23|}}}
| data41 = {{{data23|}}}
| label42 = {{{lbl24|}}}
| data42 = {{{data24|}}}
| label43 = {{{lbl25|}}}
| data43 = {{{data25|}}}
| header50 = {{#if: {{{data31|}}} {{{data32|}}} {{{data33|}}} {{{data34|}}} {{{data35|}}} | {{{extra-hdr|}}} }}
| label51 = {{{lbl31|}}}
| data51 = {{{data31|}}}
| label52 = {{{lbl32|}}}
| data52 = {{{data32|}}}
| label53 = {{{lbl33|}}}
| data53 = {{{data33|}}}
| label54 = {{{lbl34|}}}
| data54 = {{{data34|}}}
| label55 = {{{lbl35|}}}
| data55 = {{{data35|}}}
}}<noinclude>
{{Documentation}}
<!-- Add categories to the /doc subpage, not here! -->
</noinclude>
3ce93cfa3dfb9b4b9608b850f3d4e6ac282b3bc9
26
25
2023-08-27T19:15:56Z
Alxira5
4
1 revision imported: Template imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
{{Infobox
| bodystyle = border-spacing: 2px 5px;
| above = {{If empty |{{{name|}}} |<includeonly>{{PAGENAMEBASE}}</includeonly> }}
| abovestyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} | color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }}
| subheader = {{#if: {{{series|}}}{{{franchise|}}} | {{#if: {{{series|}}} | ''{{{series|}}}'' | {{{franchise|}}} }} character{{#if: {{{multiple|}}} | s }} }}
| image = {{{image|}}}|
| caption = {{{caption|}}}
| headerstyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} |color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }}
| label1 = First appearance
| data1 = {{#if: {{{first|}}} | {{{first|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{first_major|}}} |minor_work={{{first_minor|}}} |issue={{{first_issue|}}} |date={{{first_date|}}} }} }}
| label2 = First game
| data2 = {{{firstgame|}}}
| label3 = Last appearance
| data3 = {{#if: {{{last|}}} | {{{last|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{last_major|}}} |minor_work={{{last_minor|}}} |issue={{{last_issue|}}} |date={{{last_date|}}} }} }}
| label4 = Created by
| data4 = {{{creator|}}}
| label5 = Based on
| data5 = {{{based_on|}}}
| label6 = Adapted by
| data6 = {{{adapted_by|}}}
| label7 = Designed by
| data7 = {{{designer|}}}
| label8 = Portrayed by
| data8 = {{{portrayer|}}}
| label9 = Voiced by
| data9 = {{{voice|}}}
| label10 = Motion capture
| data10 = {{{motion_actor|}}}
| label11 = {{{lbl1|}}}
| data11 = {{{data1|}}}
| label12 = {{{lbl2|}}}
| data12 = {{{data2|}}}
| label13 = {{{lbl3|}}}
| data13 = {{{data3|}}}
| label14 = {{{lbl4|}}}
| data14 = {{{data4|}}}
| label15 = {{{lbl5|}}}
| data15 = {{{data5|}}}
| header20 = {{#if: {{{noinfo|}}} || {{#if: {{{full_name|}}} {{{alias|}}} {{{aliases|}}} {{{nickname|}}} {{{nicknames|}}} {{{race|}}} {{{species|}}} {{{gender|}}} {{{title|}}} {{{occupation|}}} {{{position|}}} {{{class|}}} {{{affiliation|}}} {{{alignment|}}} {{{fighting_style|}}} {{{weapon|}}} {{{family|}}} {{{spouse|}}} {{{spouses|}}} {{{significant_other|}}} {{{significant_others|}}} {{{children|}}} {{{relatives|}}} {{{religion|}}} {{{origin|}}} {{{home|}}} {{{nationality|}}} {{{data21|}}} {{{data22|}}} {{{data23|}}} {{{data24|}}} {{{data25|}}} | {{If empty |{{{info-hdr|}}} |In-universe information }} }} }}
| label21 = Full name
| data21 = {{{full_name|}}}
| label22 = {{#if: {{{alias|}}} | Alias | Aliases }}
| data22 = {{If empty |{{{alias|}}} |{{{aliases|}}} }}
| label23 = {{#if: {{{nickname|}}} | Nickname | Nicknames }}
| data23 = {{If empty |{{{nickname|}}} |{{{nicknames|}}} }}
| label24 = {{#if: {{{race|}}} | Race | Species }}
| data24 = {{If empty |{{{race|}}} |{{{species|}}} }}
| label25 = Gender
| data25 = {{{gender|}}}
| label26 = Title
| data26 = {{{title|}}}
| label27 = {{#if: {{{occupation|}}} | Occupation | {{#if: {{{position|}}} | Position | Class }} }}
| data27 = {{If empty |{{{occupation|}}} |{{{position|}}} |{{{class|}}} }}
| label28 = {{#if: {{{affiliation|}}} | Affiliation | Alignment }}
| data28 = {{If empty |{{{affiliation|}}} |{{{alignment|}}} }}
| label29 = Fighting style
| data29 = {{{fighting_style|}}}
| label30 = Weapon
| data30 = {{{weapon|}}}
| label31 = Family
| data31 = {{{family|}}}
| label32 = {{#if: {{{spouse|}}} | Spouse | Spouses }}
| data32 = {{If empty |{{{spouse|}}} |{{{spouses|}}} }}
| label33 = {{#if: {{{significant_other|}}} | Significant other | Significant others }}
| data33 = {{If empty |{{{significant_other|}}} |{{{significant_others|}}} }}
| label34 = Children
| data34 = {{{children|}}}
| label35 = Relatives
| data35 = {{{relatives|}}}
| label36 = Religion
| data36 = {{{religion|}}}
| label37 = {{#if: {{{origin|}}} | Origin | Home }}
| data37 = {{If empty |{{{origin|}}} |{{{home|}}} }}
| label38 = Nationality
| data38 = {{{nationality|}}}
| label39 = {{{lbl21|}}}
| data39 = {{{data21|}}}
| label40 = {{{lbl22|}}}
| data40 = {{{data22|}}}
| label41 = {{{lbl23|}}}
| data41 = {{{data23|}}}
| label42 = {{{lbl24|}}}
| data42 = {{{data24|}}}
| label43 = {{{lbl25|}}}
| data43 = {{{data25|}}}
| header50 = {{#if: {{{data31|}}} {{{data32|}}} {{{data33|}}} {{{data34|}}} {{{data35|}}} | {{{extra-hdr|}}} }}
| label51 = {{{lbl31|}}}
| data51 = {{{data31|}}}
| label52 = {{{lbl32|}}}
| data52 = {{{data32|}}}
| label53 = {{{lbl33|}}}
| data53 = {{{data33|}}}
| label54 = {{{lbl34|}}}
| data54 = {{{data34|}}}
| label55 = {{{lbl35|}}}
| data55 = {{{data35|}}}
}}<noinclude>
{{Documentation}}
<!-- Add categories to the /doc subpage, not here! -->
</noinclude>
3ce93cfa3dfb9b4b9608b850f3d4e6ac282b3bc9
Module:Formatted appearance
828
89
170
2023-03-21T04:35:42Z
dev>Pppery
0
Scribunto
text/plain
-- This module requires the use of Module:List.
local p = {}
-- Local function which is used to get a correctly formatted entry.
-- Function checks if the array had a value added by checking the counter,
-- and returns the relevant result.
local function getFormattedEntry(args, counter)
if (counter == 1) then -- Check if the counter stayed the same.
return "" -- Nothing was added to array; Return empty string.
elseif (counter == 2) then -- Check if only one value was added to the array.
return args[1] -- Only one value was added to array; Return that value.
else -- The array had more than one value added.
return table.concat(args, "<br/>") -- Tetrieve the formatted plainlist.
end
end
--[[
Local function which is used to format an appearance for a comic book,
in the style of:
Line 1: <comic book title> #<issue number> (with comic book title in italics)
Line 2: <release date>
For other usages, see createGenericEntry().
The function works with the following combinations:
-- Only comic book title (example: "The Incredible Hulk").
-- Title and issue number (example: "The Incredible Hulk" and "181").
-- Title and release date (example: "The Incredible Hulk and "November 1974").
-- Title, issue number and release date (example: "The Incredible Hulk", "181" and "November 1974").
-- Only release date (example: "November 1974").
--]]
local function createComicEntry(appearanceMajor, appearanceMinor, appearanceDate)
local fullString = {} -- Variable to save the array.
local counter = 1 -- Variable to save the array counter.
if (appearanceMajor ~= nil) then -- Check if a comic book title was entered.
if (appearanceMinor == nil) then -- A comic book title was entered; Check if a issue number was entered.
fullString[counter] = appearanceMajor -- A issue was not entered; Add only the comic book title to the array.
counter = counter + 1 -- Increment counter by one.
else
fullString[counter] = appearanceMajor .. " " .. appearanceMinor -- A issue was entered; Add both to the array.
counter = counter + 1 -- Increment counter by one.
end
end
if (appearanceDate ~= nil) then -- Check if a release date was entered.
fullString[counter] = appearanceDate -- A release date was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry.
end
--[[
Local function which is used to format an appearance for most usages,
including television, film, books, songs and games, in the style of:
Line 1: <minor work title> (in quotes) (Minor works include: TV episodes, chapters, songs and game missions)
Line 2: <major work title> (in italics) (Major works include: TV series, films, books, albums and games)
Line 3: <release date>
For comic book usages, see createComicEntry().
The function works with the following combinations:
-- Only minor work title (example: "Live Together, Die Alone").
-- Minor work title and major work title (example: "Live Together, Die Alone" and "Lost").
-- Minor work title and release date (example: "Live Together, Die Alone" and "May 24, 2006").
-- Minor work title, major work title and release date (example: "Live Together, Die Alone", "Lost" and "May 24, 2006").
-- Only major work title (example: "Lost").
-- major work title and release date (example: "Lost" and "May 24, 2006").
-- Only release date (example: "May 24, 2006").
--]]
local function createGenericEntry(appearanceMajor, appearanceMinor, appearanceDate)
local fullString = {} -- Variable to save the array.
local counter = 1 -- Variable to save the array counter.
if (appearanceMinor ~= nil) then -- Check if a minor appearance was entered.
fullString[counter] = appearanceMinor -- A minor appearance was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
if (appearanceMajor ~= nil) then -- Check if a major appearance was entered.
fullString[counter] = appearanceMajor -- A major appearance was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
if (appearanceDate ~= nil) then -- Check if a release date was entered.
fullString[counter] = appearanceDate -- A release date was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry.
end
-- Local function which is used to format with a hash symbol comic book issues.
-- For other minor works, see getFormattedGenericMinorWork().
local function getFormattedComicMinorWorkTitle(issue)
if (issue ~= nil) then -- Check if the issue is not nil.
if (string.find(issue, "#")) then -- Check if the issue already has a hash symbol.
return issue -- Hash symbol already present; Return issue.
else
local formattedString = string.gsub(issue, "%d+", "#%1") -- Hash symbol not found; Add the symbol before the issue number.
return formattedString -- Return issue.
end
else
return nil -- issue is nil; Return nil.
end
end
-- Local function which is used to format with quotes a minor work title of most types.
-- For comic book issues, see getFormattedComicMinorWork() (see [MOS:MINORWORK]).
local function getFormattedGenericMinorWorkTitle(title)
if (title ~= nil) then -- Check if the title is not nil.
return "\"" .. title .. "\"" -- Title is not nil; Add quotes to the title.
else
return nil -- Title is nil; Return nil.
end
end
-- Local function which is used to format with italics a major work title (see [MOS:MAJORWORK]).
local function getFormattedMajorWorkTitle(title)
if (title ~= nil) then -- Check if the title is not nil.
return "''" .. title .. "''" -- Title is not nil; Add italics to the title.
else
return nil -- Title is nil; Return nil.
end
end
-- Local function which does the actual main process.
local function _getFormattedAppearance(args)
local appearanceMajor = args['major_work'] -- Get the title of the major work.
local appearanceMinor = args['minor_work'] -- Get the title of the minor work.
local isComic = false -- Variable to save the status of wether the appearence is from a comic book.
if (args['issue'] ~= nil) then -- Check if the comic specific issue is not nil.
appearanceMinor = args['issue'] -- Issue is not nil; Get the issue number.
isComic = true -- Set isComic to true.
end
local appearanceDate = args['date'] -- Get the release date of the minor work.
local formattedAppearanceMajor = getFormattedMajorWorkTitle(appearanceMajor) -- Call getFormattedMajorWorkTitle() to get a formatted major work title.
if (isComic == false) then -- Check if the appearance is a comic book appearance.
-- The appearance is not a comic book appearance;
local formattedAppearanceMinor = getFormattedGenericMinorWorkTitle(appearanceMinor) -- Call getFormattedGenericMinorWorkTitle() to get a formatted minor work title.
return createGenericEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createGenericEntry() to create an appearance entry.
else
-- The appearance is a comic book appearance.
local formattedAppearanceMinor = getFormattedComicMinorWorkTitle(appearanceMinor) -- Call getFormattedComicMinorWorkTitle() to get a formatted minor work title.
return createComicEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createComicEntry() to create a comic book appearance entry.
end
end
--[[
Public function which is used to format the |first_appeared= and |last_appeared= fields.
The usage of this module allows for correct title formatting (see [MOS:MAJORWORK] and [MOS:MINORWORK]),
and correct line breaks based on guidelines (see [WP:UBLIST]).
Parameters:
-- |major_work= — optional; The title of the major work the fictional element appeared in.
Major works include TV series, films, books, albums and games.
-- |minor_work= — optional; The title of the minor work the fictional element appeared in.
Minor works include TV episodes, chapters, songs and game missions.
-- |issue= — optional; The number of the comic book issue the fictional element appeared in.
-- |date= — optional; The date of the publication/release of the minor work where the fictional element appeared in.
--]]
function p.getFormattedAppearance(frame)
local getArgs = require('Module:Arguments').getArgs -- Use Module:Arguments to access module arguments.
local args = getArgs(frame) -- Get the arguments sent via the template.
return _getFormattedAppearance(args) -- Call _getFormattedAppearance() to perform the actual process.
end
return p
983d4add2379f19ec30241c0470bf9b6c4089eb2
Main Page
0
1
1
2023-07-25T19:05:26Z
MediaWiki default
1
Create main page
wikitext
text/x-wiki
__NOTOC__
== Welcome to {{SITENAME}}! ==
This Main Page was created automatically and it seems it hasn't been replaced yet.
=== For the bureaucrat(s) of this wiki ===
Hello, and welcome to your new wiki! Thank you for choosing Miraheze for the hosting of your wiki, we hope you will enjoy our hosting.
You can immediately start working on your wiki or whenever you want.
Need help? No problem! We will help you with your wiki as needed. To start, try checking out these helpful links:
* [[mw:Special:MyLanguage/Help:Contents|MediaWiki guide]] (e.g. navigation, editing, deleting pages, blocking users)
* [[meta:Special:MyLanguage/FAQ|Miraheze FAQ]]
* [[meta:Special:MyLanguage/Request features|Request settings changes on your wiki]]. (Extensions, Skin and Logo/Favicon changes should be done through [[Special:ManageWiki]] on your wiki, see [[meta:Special:MyLanguage/ManageWiki|ManageWiki]] for more information.)
==== I still don't understand X! ====
Well, that's no problem. Even if something isn't explained in the documentation/FAQ, we are still happy to help you. You can find us here:
* [[meta:Special:MyLanguage/Help center|On our own Miraheze wiki]]
* On [[phab:|Phabricator]]
* On [https://miraheze.org/discord Discord]
* On IRC in #miraheze on irc.libera.chat ([irc://irc.libera.chat/%23miraheze direct link]; [https://web.libera.chat/?channel=#miraheze webchat])
=== For visitors of this wiki ===
Hello, the default Main Page of this wiki (this page) has not yet been replaced by the bureaucrat(s) of this wiki. The bureaucrat(s) might still be working on a Main Page, so please check again later!
21236ac3f8d65e5563b6da6b70815ca6bf1e6616
Template:FlowMention
10
2
2
2023-07-25T19:42:04Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
@[[User:{{{1|Example}}}|{{{2|{{{1|Example}}}}}}]]
98786e33cb63444ac23e3cf2bdbcab2d9501a6e7
Template:LQT Moved thread stub converted to Flow
10
3
3
2023-07-25T19:42:05Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
This post by {{{author}}} was moved on {{{date}}}. You can find it at [[{{{title}}}]].
792a92295d0603dc3cb5c46e15d4e42af9659414
Template:LQT page converted to Flow
10
4
4
2023-07-25T19:42:05Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
Previous page history was archived for backup purposes at <span class='flow-link-to-archive'>[[{{{archive}}}]]</span> on {{#time: Y-m-d|{{{date}}}}}.
c988d60d1df8c49bcce1f4f94a9c2a5318faf0d3
Template:Archive for converted LQT page
10
5
5
2023-07-25T19:42:06Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
This page is an archived LiquidThreads page. <strong>Do not edit the contents of this page</strong>. Please direct any additional comments to the [[{{{from}}}|current talk page]].
6f2232948be664f5eec18e4b7a6219814d38a478
Template:LQT post imported with suppressed user
10
6
6
2023-07-25T19:42:07Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
This revision was imported from LiquidThreads with a suppressed user. It has been reassigned to the current user.
0eb25fe53f4e146ddc0b16b14bd40d6069e56c06
Template:LQT post imported with different signature user
10
7
7
2023-07-25T19:42:07Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
<em>This post was posted by [[User:{{{authorUser}}}|{{{authorUser}}}]], but signed as [[User:{{{signatureUser}}}|{{{signatureUser}}}]].</em>
047294b02240e1b8526ad076eb47a07e98747bac
Template:Wikitext talk page converted to Flow
10
8
8
2023-07-25T19:42:08Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
Previous discussion was archived at <span class='flow-link-to-archive'>[[{{{archive}}}]]</span> on {{#time: Y-m-d|{{{date}}}}}.
69c9712008fdef423f0f0332a7d4ffcfe65e6e76
Template:Archive for converted wikitext talk page
10
9
9
2023-07-25T19:42:08Z
Flow talk page manager
3
/* Automatically created by Flow */
wikitext
text/x-wiki
This page is an archive. <strong>Do not edit the contents of this page</strong>. Please direct any additional comments to the [[{{{from|{{TALKSPACE}}:{{BASEPAGENAME}}}}}|current talk page]].
de059e2d945be0557b47d689299d8bd96e9699ed
Special:Badtitle/NS200:AlexGX22
200
10
10
2023-07-25T21:09:56Z
AlXGX21
2
import user wiki
wikitext
text/x-wiki
da39a3ee5e6b4b0d3255bfef95601890afd80709
User:AlXGX21
2
14
23
2023-08-27T17:41:26Z
AlXGX21
2
Redirection to my new account
wikitext
text/x-wiki
#REDIRECT [[User:Alxira5|</nowiki>Alxira5]]
5cd0368e4dea8043dc1782a4946317e31e03d2a2
MediaWiki:Common.css
8
15
24
2023-08-27T18:54:13Z
Alxira5
4
CSS file imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
css
text/css
/* Default style for navigation boxes */
.navbox { /* Navbox container style */
box-sizing: border-box;
border: 1px solid #a2a9b1;
width: 100%;
clear: both;
font-size: 88%;
text-align: center;
padding: 1px;
margin: 1em auto 0; /* Prevent preceding content from clinging to navboxes */
}
.navbox .navbox {
margin-top: 0; /* No top margin for nested navboxes */
}
.navbox + .navbox {
margin-top: -1px; /* Single pixel border between adjacent navboxes */
}
.navbox-inner,
.navbox-subgroup {
width: 100%;
}
.navbox-group,
.navbox-title,
.navbox-abovebelow {
padding: 0.25em 1em; /* Title, group and above/below styles */
line-height: 1.5em;
text-align: center;
}
th.navbox-group { /* Group style */
white-space: nowrap;
/* @noflip */
text-align: right;
}
.navbox,
.navbox-subgroup {
background-color: #fdfdfd; /* Background color */
}
.navbox-list {
line-height: 1.5em;
border-color: #fdfdfd; /* Must match background color */
}
/* cell spacing for navbox cells */
tr + tr > .navbox-abovebelow,
tr + tr > .navbox-group,
tr + tr > .navbox-image,
tr + tr > .navbox-list { /* Borders above 2nd, 3rd, etc. rows */
border-top: 2px solid #fdfdfd; /* Must match background color */
}
.navbox th,
.navbox-title {
background-color: #ccccff; /* Level 1 color */
}
.navbox-abovebelow,
th.navbox-group,
.navbox-subgroup .navbox-title {
background-color: #ddddff; /* Level 2 color */
}
.navbox-subgroup .navbox-group,
.navbox-subgroup .navbox-abovebelow {
background-color: #e6e6ff; /* Level 3 color */
}
.navbox-even {
background-color: #f7f7f7; /* Even row striping */
}
.navbox-odd {
background-color: transparent; /* Odd row striping */
}
.navbox .hlist td dl,
.navbox .hlist td ol,
.navbox .hlist td ul,
.navbox td.hlist dl,
.navbox td.hlist ol,
.navbox td.hlist ul {
padding: 0.125em 0; /* Adjust hlist padding in navboxes */
}
/* Default styling for Navbar template */
.navbar {
display: inline;
font-size: 88%;
font-weight: normal;
}
.navbar ul {
display: inline;
white-space: nowrap;
}
.mw-body-content .navbar ul {
line-height: inherit;
}
.navbar li {
word-spacing: -0.125em;
}
.navbar.mini li abbr[title] {
font-variant: small-caps;
border-bottom: none;
text-decoration: none;
cursor: inherit;
}
/* Navbar styling when nested in infobox and navbox */
.infobox .navbar {
font-size: 100%;
}
.navbox .navbar {
display: block;
font-size: 100%;
}
.navbox-title .navbar {
/* @noflip */
float: left;
/* @noflip */
text-align: left;
/* @noflip */
margin-right: 0.5em;
}
.infobox {
border: 1px solid #a2a9b1;
border-spacing: 3px;
background-color: #f8f9fa;
color: black;
/* @noflip */
margin: 0.5em 0 0.5em 1em;
padding: 0.2em;
/* @noflip */
float: right;
/* @noflip */
clear: right;
font-size: 88%;
line-height: 1.5em;
}
.infobox caption {
font-size: 125%;
font-weight: bold;
padding: 0.2em;
text-align: center;
}
.infobox td,
.infobox th {
vertical-align: top;
/* @noflip */
text-align: left;
}
.infobox.bordered {
border-collapse: collapse;
}
.infobox.bordered td,
.infobox.bordered th {
border: 1px solid #a2a9b1;
}
.infobox.bordered .borderless td,
.infobox.bordered .borderless th {
border: 0;
}
.infobox.sisterproject {
width: 20em;
font-size: 90%;
}
.infobox.standard-talk {
border: 1px solid #c0c090;
background-color: #f8eaba;
}
.infobox.standard-talk.bordered td,
.infobox.standard-talk.bordered th {
border: 1px solid #c0c090;
}
/* styles for bordered infobox with merged rows */
.infobox.bordered .mergedtoprow td,
.infobox.bordered .mergedtoprow th {
border: 0;
border-top: 1px solid #a2a9b1;
/* @noflip */
border-right: 1px solid #a2a9b1;
}
.infobox.bordered .mergedrow td,
.infobox.bordered .mergedrow th {
border: 0;
/* @noflip */
border-right: 1px solid #a2a9b1;
}
/* Styles for geography infoboxes, eg countries,
country subdivisions, cities, etc. */
.infobox.geography {
border-collapse: collapse;
line-height: 1.2em;
font-size: 90%;
}
.infobox.geography td,
.infobox.geography th {
border-top: 1px solid #a2a9b1;
padding: 0.4em 0.6em 0.4em 0.6em;
}
.infobox.geography .mergedtoprow td,
.infobox.geography .mergedtoprow th {
border-top: 1px solid #a2a9b1;
padding: 0.4em 0.6em 0.2em 0.6em;
}
.infobox.geography .mergedrow td,
.infobox.geography .mergedrow th {
border: 0;
padding: 0 0.6em 0.2em 0.6em;
}
.infobox.geography .mergedbottomrow td,
.infobox.geography .mergedbottomrow th {
border-top: 0;
border-bottom: 1px solid #a2a9b1;
padding: 0 0.6em 0.4em 0.6em;
}
.infobox.geography .maptable td,
.infobox.geography .maptable th {
border: 0;
padding: 0;
}
div.listenlist {
background: url("//upload.wikimedia.org/wikipedia/commons/4/47/Sound-icon.svg") no-repeat scroll 0 0 transparent;
background-size: 30px;
padding-left: 40px;
}
/* Fix for hieroglyphs specificity issue in infoboxes ([[phab:T43869]]) */
table.mw-hiero-table td {
vertical-align: middle;
}
/* Style rules for media list templates */
/* TemplateStyles */
div.medialist {
min-height: 50px;
margin: 1em;
/* @noflip */
background-position: top left;
background-repeat: no-repeat;
}
div.medialist ul {
list-style-type: none;
list-style-image: none;
margin: 0;
}
div.medialist ul li {
padding-bottom: 0.5em;
}
div.medialist ul li li {
font-size: 91%;
padding-bottom: 0;
}
/* Messagebox templates */
.messagebox {
border: 1px solid #a2a9b1;
background-color: #f8f9fa;
width: 80%;
margin: 0 auto 1em auto;
padding: .2em;
}
.messagebox.merge {
border: 1px solid #c0b8cc;
background-color: #f0e5ff;
text-align: center;
}
.messagebox.cleanup {
border: 1px solid #9f9fff;
background-color: #efefff;
text-align: center;
}
.messagebox.standard-talk {
border: 1px solid #c0c090;
background-color: #f8eaba;
margin: 4px auto;
}
/* For old WikiProject banners inside banner shells. */
.mbox-inside .standard-talk,
.messagebox.nested-talk {
border: 1px solid #c0c090;
background-color: #f8eaba;
width: 100%;
margin: 2px 0;
padding: 2px;
}
.messagebox.small {
width: 238px;
font-size: 85%;
/* @noflip */
float: right;
clear: both;
/* @noflip */
margin: 0 0 1em 1em;
line-height: 1.25em;
}
.messagebox.small-talk {
width: 238px;
font-size: 85%;
/* @noflip */
float: right;
clear: both;
/* @noflip */
margin: 0 0 1em 1em;
line-height: 1.25em;
background-color: #F8EABA;
}
/* Cell sizes for ambox/tmbox/imbox/cmbox/ombox/fmbox/dmbox message boxes */
th.mbox-text, td.mbox-text { /* The message body cell(s) */
border: none;
/* @noflip */
padding: 0.25em 0.9em; /* 0.9em left/right */
width: 100%; /* Make all mboxes the same width regardless of text length */
}
td.mbox-image { /* The left image cell */
border: none;
/* @noflip */
padding: 2px 0 2px 0.9em; /* 0.9em left, 0px right */
text-align: center;
}
td.mbox-imageright { /* The right image cell */
border: none;
/* @noflip */
padding: 2px 0.9em 2px 0; /* 0px left, 0.9em right */
text-align: center;
}
td.mbox-empty-cell { /* An empty narrow cell */
border: none;
padding: 0;
width: 1px;
}
/* Article message box styles */
table.ambox {
margin: 0 10%; /* 10% = Will not overlap with other elements */
border: 1px solid #a2a9b1;
/* @noflip */
border-left: 10px solid #36c; /* Default "notice" blue */
background-color: #fbfbfb;
box-sizing: border-box;
}
table.ambox + table.ambox { /* Single border between stacked boxes. */
margin-top: -1px;
}
.ambox th.mbox-text,
.ambox td.mbox-text { /* The message body cell(s) */
padding: 0.25em 0.5em; /* 0.5em left/right */
}
.ambox td.mbox-image { /* The left image cell */
/* @noflip */
padding: 2px 0 2px 0.5em; /* 0.5em left, 0px right */
}
.ambox td.mbox-imageright { /* The right image cell */
/* @noflip */
padding: 2px 0.5em 2px 0; /* 0px left, 0.5em right */
}
table.ambox-notice {
/* @noflip */
border-left: 10px solid #36c; /* Blue */
}
table.ambox-speedy {
/* @noflip */
border-left: 10px solid #b32424; /* Red */
background-color: #fee7e6; /* Pink */
}
table.ambox-delete {
/* @noflip */
border-left: 10px solid #b32424; /* Red */
}
table.ambox-content {
/* @noflip */
border-left: 10px solid #f28500; /* Orange */
}
table.ambox-style {
/* @noflip */
border-left: 10px solid #fc3; /* Yellow */
}
table.ambox-move {
/* @noflip */
border-left: 10px solid #9932cc; /* Purple */
}
table.ambox-protection {
/* @noflip */
border-left: 10px solid #a2a9b1; /* Gray-gold */
}
/* Image message box styles */
table.imbox {
margin: 4px 10%;
border-collapse: collapse;
border: 3px solid #36c; /* Default "notice" blue */
background-color: #fbfbfb;
box-sizing: border-box;
}
.imbox .mbox-text .imbox { /* For imboxes inside imbox-text cells. */
margin: 0 -0.5em; /* 0.9 - 0.5 = 0.4em left/right. */
display: block; /* Fix for webkit to force 100% width. */
}
.mbox-inside .imbox { /* For imboxes inside other templates. */
margin: 4px;
}
table.imbox-notice {
border: 3px solid #36c; /* Blue */
}
table.imbox-speedy {
border: 3px solid #b32424; /* Red */
background-color: #fee7e6; /* Pink */
}
table.imbox-delete {
border: 3px solid #b32424; /* Red */
}
table.imbox-content {
border: 3px solid #f28500; /* Orange */
}
table.imbox-style {
border: 3px solid #fc3; /* Yellow */
}
table.imbox-move {
border: 3px solid #9932cc; /* Purple */
}
table.imbox-protection {
border: 3px solid #a2a9b1; /* Gray-gold */
}
table.imbox-license {
border: 3px solid #88a; /* Dark gray */
background-color: #f7f8ff; /* Light gray */
}
table.imbox-featured {
border: 3px solid #cba135; /* Brown-gold */
}
/* Category message box styles */
table.cmbox {
margin: 3px 10%;
border-collapse: collapse;
border: 1px solid #a2a9b1;
background-color: #DFE8FF; /* Default "notice" blue */
box-sizing: border-box;
}
table.cmbox-notice {
background-color: #D8E8FF; /* Blue */
}
table.cmbox-speedy {
margin-top: 4px;
margin-bottom: 4px;
border: 4px solid #b32424; /* Red */
background-color: #FFDBDB; /* Pink */
}
table.cmbox-delete {
background-color: #FFDBDB; /* Red */
}
table.cmbox-content {
background-color: #FFE7CE; /* Orange */
}
table.cmbox-style {
background-color: #FFF9DB; /* Yellow */
}
table.cmbox-move {
background-color: #E4D8FF; /* Purple */
}
table.cmbox-protection {
background-color: #EFEFE1; /* Gray-gold */
}
/* Other pages message box styles */
table.ombox {
margin: 4px 10%;
border-collapse: collapse;
border: 1px solid #a2a9b1; /* Default "notice" gray */
background-color: #f8f9fa;
box-sizing: border-box;
}
table.ombox-notice {
border: 1px solid #a2a9b1; /* Gray */
}
table.ombox-speedy {
border: 2px solid #b32424; /* Red */
background-color: #fee7e6; /* Pink */
}
table.ombox-delete {
border: 2px solid #b32424; /* Red */
}
table.ombox-content {
border: 1px solid #f28500; /* Orange */
}
table.ombox-style {
border: 1px solid #fc3; /* Yellow */
}
table.ombox-move {
border: 1px solid #9932cc; /* Purple */
}
table.ombox-protection {
border: 2px solid #a2a9b1; /* Gray-gold */
}
/* Talk page message box styles */
table.tmbox {
margin: 4px 10%;
border-collapse: collapse;
border: 1px solid #c0c090; /* Default "notice" gray-brown */
background-color: #f8eaba;
min-width: 80%;
box-sizing: border-box;
}
.tmbox.mbox-small {
min-width: 0; /* reset the min-width of tmbox above */
}
.mediawiki .mbox-inside .tmbox { /* For tmboxes inside other templates. The "mediawiki" class ensures that */
margin: 2px 0; /* this declaration overrides other styles (including mbox-small above) */
width: 100%; /* For Safari and Opera */
}
.mbox-inside .tmbox.mbox-small { /* "small" tmboxes should not be small when */
line-height: 1.5em; /* also "nested", so reset styles that are */
font-size: 100%; /* set in "mbox-small" above. */
}
table.tmbox-speedy {
border: 2px solid #b32424; /* Red */
background-color: #fee7e6; /* Pink */
}
table.tmbox-delete {
border: 2px solid #b32424; /* Red */
}
table.tmbox-content {
border: 2px solid #f28500; /* Orange */
}
table.tmbox-style {
border: 2px solid #fc3; /* Yellow */
}
table.tmbox-move {
border: 2px solid #9932cc; /* Purple */
}
table.tmbox-protection,
table.tmbox-notice {
border: 1px solid #c0c090; /* Gray-brown */
}
/* Disambig and set index box styles */
table.dmbox {
clear: both;
margin: 0.9em 1em;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
background-color: transparent;
}
/* Footer and header message box styles */
table.fmbox {
clear: both;
margin: 0.2em 0;
width: 100%;
border: 1px solid #a2a9b1;
background-color: #f8f9fa; /* Default "system" gray */
box-sizing: border-box;
}
table.fmbox-system {
background-color: #f8f9fa;
}
table.fmbox-warning {
border: 1px solid #bb7070; /* Dark pink */
background-color: #ffdbdb; /* Pink */
}
table.fmbox-editnotice {
background-color: transparent;
}
/* Div based "warning" style fmbox messages. */
div.mw-warning-with-logexcerpt,
div.mw-lag-warn-high,
div.mw-cascadeprotectedwarning,
div#mw-protect-cascadeon,
div.titleblacklist-warning,
div.locked-warning {
clear: both;
margin: 0.2em 0;
border: 1px solid #bb7070;
background-color: #ffdbdb;
padding: 0.25em 0.9em;
box-sizing: border-box;
}
/* These mbox-small classes must be placed after all other
ambox/tmbox/ombox etc classes. "html body.mediawiki" is so
they override "table.ambox + table.ambox" above. */
html body.mediawiki .mbox-small { /* For the "small=yes" option. */
/* @noflip */
clear: right;
/* @noflip */
float: right;
/* @noflip */
margin: 4px 0 4px 1em;
box-sizing: border-box;
width: 238px;
font-size: 88%;
line-height: 1.25em;
}
html body.mediawiki .mbox-small-left { /* For the "small=left" option. */
/* @noflip */
margin: 4px 1em 4px 0;
box-sizing: border-box;
overflow: hidden;
width: 238px;
border-collapse: collapse;
font-size: 88%;
line-height: 1.25em;
}
/* Style for compact ambox */
/* Hide the images */
.compact-ambox table .mbox-image,
.compact-ambox table .mbox-imageright,
.compact-ambox table .mbox-empty-cell {
display: none;
}
/* Remove borders, backgrounds, padding, etc. */
.compact-ambox table.ambox {
border: none;
border-collapse: collapse;
background-color: transparent;
margin: 0 0 0 1.6em !important;
padding: 0 !important;
width: auto;
display: block;
}
body.mediawiki .compact-ambox table.mbox-small-left {
font-size: 100%;
width: auto;
margin: 0;
}
/* Style the text cell as a list item and remove its padding */
.compact-ambox table .mbox-text {
padding: 0 !important;
margin: 0 !important;
}
.compact-ambox table .mbox-text-span {
display: list-item;
line-height: 1.5em;
list-style-type: square;
list-style-image: url(/w/skins/MonoBook/resources/images/bullet.gif);
}
.skin-vector .compact-ambox table .mbox-text-span {
list-style-type: disc;
list-style-image: url(/w/skins/Vector/images/bullet-icon.svg);
list-style-image: url(/w/skins/Vector/images/bullet-icon.png)\9;
}
/* Allow for hiding text in compact form */
.compact-ambox .hide-when-compact {
display: none;
}
/* For template documentation */
/* TemplateStyles */
.template-documentation {
clear: both;
margin: 1em 0 0 0;
border: 1px solid #a2a9b1;
background-color: #ecfcf4;
padding: 1em;
}
d9e4c7221dafae29c01080b36effd079723d287b
Template:Infobox
10
17
28
27
2023-08-27T19:18:34Z
Alxira5
4
1 revision imported: Templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
{{#invoke:Infobox|infobox}}<noinclude>
{{documentation}}
</noinclude>
627ee6fcf4d4f108fe054b5c476201cad0ed7717
Template:Documentation
10
18
30
29
2023-08-27T19:18:35Z
Alxira5
4
1 revision imported: Templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
{{#invoke:documentation|main|_content={{ {{#invoke:documentation|contentTitle}}}}}}<noinclude>[[Category:Templates]]</noinclude>
9885bb4fa99bf3d5b960e73606bbb8eed3026877
Module:Infobox
828
19
32
31
2023-08-27T19:26:21Z
Alxira5
4
1 revision imported: Modules imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
Scribunto
text/plain
--
-- This module implements {{Infobox}}
--
local p = {}
local args = {}
local origArgs = {}
local root
local function notempty( s ) return s and s:match( '%S' ) end
local function fixChildBoxes(sval, tt)
if notempty(sval) then
local marker = '<span class=special_infobox_marker>'
local s = sval
s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1')
s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker)
if s:match(marker) then
s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '')
s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1')
s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1')
s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1')
s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1')
end
if s:match(marker) then
local subcells = mw.text.split(s, marker)
s = ''
for k = 1, #subcells do
if k == 1 then
s = s .. subcells[k] .. '</' .. tt .. '></tr>'
elseif k == #subcells then
local rowstyle = ' style="display:none"'
if notempty(subcells[k]) then rowstyle = '' end
s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' .. subcells[k]
elseif notempty(subcells[k]) then
if (k % 2) == 0 then
s = s .. subcells[k]
else
s = s .. '<tr><' .. tt .. ' colspan=2>\n' .. subcells[k] .. '</' .. tt .. '></tr>'
end
end
end
end
-- the next two lines add a newline at the end of lists for the PHP parser
-- https://en.wikipedia.org/w/index.php?title=Template_talk:Infobox_musical_artist&oldid=849054481
-- remove when [[:phab:T191516]] is fixed or OBE
s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n')
s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n')
s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1')
s = mw.ustring.gsub(s, '^(%{%|)', '\n%1')
return s
else
return sval
end
end
local function union(t1, t2)
-- Returns the union of the values of two tables, as a sequence.
local vals = {}
for k, v in pairs(t1) do
vals[v] = true
end
for k, v in pairs(t2) do
vals[v] = true
end
local ret = {}
for k, v in pairs(vals) do
table.insert(ret, k)
end
return ret
end
local function getArgNums(prefix)
-- Returns a table containing the numbers of the arguments that exist
-- for the specified prefix. For example, if the prefix was 'data', and
-- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
local nums = {}
for k, v in pairs(args) do
local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$')
if num then table.insert(nums, tonumber(num)) end
end
table.sort(nums)
return nums
end
local function addRow(rowArgs)
-- Adds a row to the infobox, with either a header cell
-- or a label/data cell combination.
if rowArgs.header and rowArgs.header ~= '_BLANK_' then
root
:tag('tr')
:addClass(rowArgs.rowclass)
:cssText(rowArgs.rowstyle)
:attr('id', rowArgs.rowid)
:tag('th')
:attr('colspan', 2)
:attr('id', rowArgs.headerid)
:addClass(rowArgs.class)
:addClass(args.headerclass)
:css('text-align', 'center')
:cssText(args.headerstyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(fixChildBoxes(rowArgs.header, 'th'))
elseif rowArgs.data then
if not rowArgs.data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then
rowArgs.rowstyle = 'display:none'
end
local row = root:tag('tr')
row:addClass(rowArgs.rowclass)
row:cssText(rowArgs.rowstyle)
row:attr('id', rowArgs.rowid)
if rowArgs.label then
row
:tag('th')
:attr('scope', 'row')
:attr('id', rowArgs.labelid)
:cssText(args.labelstyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(rowArgs.label)
:done()
end
local dataCell = row:tag('td')
if not rowArgs.label then
dataCell
:attr('colspan', 2)
:css('text-align', 'center')
end
dataCell
:attr('id', rowArgs.dataid)
:addClass(rowArgs.class)
:cssText(rowArgs.datastyle)
:cssText(rowArgs.rowcellstyle)
:wikitext(fixChildBoxes(rowArgs.data, 'td'))
end
end
local function renderTitle()
if not args.title then return end
root
:tag('caption')
:addClass(args.titleclass)
:cssText(args.titlestyle)
:wikitext(args.title)
end
local function renderAboveRow()
if not args.above then return end
root
:tag('tr')
:tag('th')
:attr('colspan', 2)
:addClass(args.aboveclass)
:css('text-align', 'center')
:css('font-size', '125%')
:css('font-weight', 'bold')
:cssText(args.abovestyle)
:wikitext(fixChildBoxes(args.above,'th'))
end
local function renderBelowRow()
if not args.below then return end
root
:tag('tr')
:tag('td')
:attr('colspan', '2')
:addClass(args.belowclass)
:css('text-align', 'center')
:cssText(args.belowstyle)
:wikitext(fixChildBoxes(args.below,'td'))
end
local function renderSubheaders()
if args.subheader then
args.subheader1 = args.subheader
end
if args.subheaderrowclass then
args.subheaderrowclass1 = args.subheaderrowclass
end
local subheadernums = getArgNums('subheader')
for k, num in ipairs(subheadernums) do
addRow({
data = args['subheader' .. tostring(num)],
datastyle = args.subheaderstyle,
rowcellstyle = args['subheaderstyle' .. tostring(num)],
class = args.subheaderclass,
rowclass = args['subheaderrowclass' .. tostring(num)]
})
end
end
local function renderImages()
if args.image then
args.image1 = args.image
end
if args.caption then
args.caption1 = args.caption
end
local imagenums = getArgNums('image')
for k, num in ipairs(imagenums) do
local caption = args['caption' .. tostring(num)]
local data = mw.html.create():wikitext(args['image' .. tostring(num)])
if caption then
data
:tag('div')
:cssText(args.captionstyle)
:wikitext(caption)
end
addRow({
data = tostring(data),
datastyle = args.imagestyle,
class = args.imageclass,
rowclass = args['imagerowclass' .. tostring(num)]
})
end
end
local function preprocessRows()
-- Gets the union of the header and data argument numbers,
-- and renders them all in order using addRow.
local rownums = union(getArgNums('header'), getArgNums('data'))
table.sort(rownums)
local lastheader
for k, num in ipairs(rownums) do
if args['header' .. tostring(num)] then
if lastheader then
args['header' .. tostring(lastheader)] = nil
end
lastheader = num
elseif args['data' .. tostring(num)] and args['data' .. tostring(num)]:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then
local data = args['data' .. tostring(num)]
if data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('%S') then
lastheader = nil
end
end
end
if lastheader then
args['header' .. tostring(lastheader)] = nil
end
end
local function renderRows()
-- Gets the union of the header and data argument numbers,
-- and renders them all in order using addRow.
local rownums = union(getArgNums('header'), getArgNums('data'))
table.sort(rownums)
for k, num in ipairs(rownums) do
addRow({
header = args['header' .. tostring(num)],
label = args['label' .. tostring(num)],
data = args['data' .. tostring(num)],
datastyle = args.datastyle,
class = args['class' .. tostring(num)],
rowclass = args['rowclass' .. tostring(num)],
rowstyle = args['rowstyle' .. tostring(num)],
rowcellstyle = args['rowcellstyle' .. tostring(num)],
dataid = args['dataid' .. tostring(num)],
labelid = args['labelid' .. tostring(num)],
headerid = args['headerid' .. tostring(num)],
rowid = args['rowid' .. tostring(num)]
})
end
end
local function renderItalicTitle()
local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title'])
if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then
root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'}))
end
end
local function _infobox()
-- Specify the overall layout of the infobox, with special settings
-- if the infobox is used as a 'child' inside another infobox.
if args.child ~= 'yes' then
root = mw.html.create('table')
root
:addClass((args.subbox ~= 'yes') and 'infobox' or nil)
:addClass(args.bodyclass)
if args.subbox == 'yes' then
root
:css('padding', '0')
:css('border', 'none')
:css('margin', '-3px')
:css('width', 'auto')
:css('min-width', '100%')
:css('font-size', '100%')
:css('clear', 'none')
:css('float', 'none')
:css('background-color', 'transparent')
else
root
:css('width', '22em')
end
root
:cssText(args.bodystyle)
renderTitle()
renderAboveRow()
else
root = mw.html.create()
root
:wikitext(args.title)
end
renderSubheaders()
renderImages()
if args.autoheaders then
preprocessRows()
end
renderRows()
renderBelowRow()
renderItalicTitle()
return tostring(root)
end
local function preprocessSingleArg(argName)
-- If the argument exists and isn't blank, add it to the argument table.
-- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
if origArgs[argName] and origArgs[argName] ~= '' then
args[argName] = origArgs[argName]
end
end
local function preprocessArgs(prefixTable, step)
-- Assign the parameters with the given prefixes to the args table, in order, in batches
-- of the step size specified. This is to prevent references etc. from appearing in the
-- wrong order. The prefixTable should be an array containing tables, each of which has
-- two possible fields, a "prefix" string and a "depend" table. The function always parses
-- parameters containing the "prefix" string, but only parses parameters in the "depend"
-- table if the prefix parameter is present and non-blank.
if type(prefixTable) ~= 'table' then
error("Non-table value detected for the prefix table", 2)
end
if type(step) ~= 'number' then
error("Invalid step value detected", 2)
end
-- Get arguments without a number suffix, and check for bad input.
for i,v in ipairs(prefixTable) do
if type(v) ~= 'table' or type(v.prefix) ~= "string" or (v.depend and type(v.depend) ~= 'table') then
error('Invalid input detected to preprocessArgs prefix table', 2)
end
preprocessSingleArg(v.prefix)
-- Only parse the depend parameter if the prefix parameter is present and not blank.
if args[v.prefix] and v.depend then
for j, dependValue in ipairs(v.depend) do
if type(dependValue) ~= 'string' then
error('Invalid "depend" parameter value detected in preprocessArgs')
end
preprocessSingleArg(dependValue)
end
end
end
-- Get arguments with number suffixes.
local a = 1 -- Counter variable.
local moreArgumentsExist = true
while moreArgumentsExist == true do
moreArgumentsExist = false
for i = a, a + step - 1 do
for j,v in ipairs(prefixTable) do
local prefixArgName = v.prefix .. tostring(i)
if origArgs[prefixArgName] then
moreArgumentsExist = true -- Do another loop if any arguments are found, even blank ones.
preprocessSingleArg(prefixArgName)
end
-- Process the depend table if the prefix argument is present and not blank, or
-- we are processing "prefix1" and "prefix" is present and not blank, and
-- if the depend table is present.
if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
for j,dependValue in ipairs(v.depend) do
local dependArgName = dependValue .. tostring(i)
preprocessSingleArg(dependArgName)
end
end
end
end
a = a + step
end
end
local function parseDataParameters()
-- Parse the data parameters in the same order that the old {{infobox}} did, so that
-- references etc. will display in the expected places. Parameters that depend on
-- another parameter are only processed if that parameter is present, to avoid
-- phantom references appearing in article reference lists.
preprocessSingleArg('autoheaders')
preprocessSingleArg('child')
preprocessSingleArg('bodyclass')
preprocessSingleArg('subbox')
preprocessSingleArg('bodystyle')
preprocessSingleArg('title')
preprocessSingleArg('titleclass')
preprocessSingleArg('titlestyle')
preprocessSingleArg('above')
preprocessSingleArg('aboveclass')
preprocessSingleArg('abovestyle')
preprocessArgs({
{prefix = 'subheader', depend = {'subheaderstyle', 'subheaderrowclass'}}
}, 10)
preprocessSingleArg('subheaderstyle')
preprocessSingleArg('subheaderclass')
preprocessArgs({
{prefix = 'image', depend = {'caption', 'imagerowclass'}}
}, 10)
preprocessSingleArg('captionstyle')
preprocessSingleArg('imagestyle')
preprocessSingleArg('imageclass')
preprocessArgs({
{prefix = 'header'},
{prefix = 'data', depend = {'label'}},
{prefix = 'rowclass'},
{prefix = 'rowstyle'},
{prefix = 'rowcellstyle'},
{prefix = 'class'},
{prefix = 'dataid'},
{prefix = 'labelid'},
{prefix = 'headerid'},
{prefix = 'rowid'}
}, 50)
preprocessSingleArg('headerclass')
preprocessSingleArg('headerstyle')
preprocessSingleArg('labelstyle')
preprocessSingleArg('datastyle')
preprocessSingleArg('below')
preprocessSingleArg('belowclass')
preprocessSingleArg('belowstyle')
preprocessSingleArg('name')
args['italic title'] = origArgs['italic title'] -- different behaviour if blank or absent
preprocessSingleArg('decat')
end
function p.infobox(frame)
-- If called via #invoke, use the args passed into the invoking template.
-- Otherwise, for testing purposes, assume args are being passed directly in.
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
else
origArgs = frame
end
parseDataParameters()
return _infobox()
end
function p.infoboxTemplate(frame)
-- For calling via #invoke within a template
origArgs = {}
for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end
parseDataParameters()
return _infobox()
end
return p
c6ac51f9e2faf9c2f3aba1fb8c05af98db47f4d4
Module:Documentation
828
20
34
33
2023-08-27T19:26:21Z
Alxira5
4
1 revision imported: Modules imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
Scribunto
text/plain
-- This module implements {{documentation}}.
-- Get required modules.
local getArgs = require('Module:Arguments').getArgs
-- Get the config table.
local cfg = mw.loadData('Module:Documentation/config')
local p = {}
-- Often-used functions.
local ugsub = mw.ustring.gsub
----------------------------------------------------------------------------
-- Helper functions
--
-- These are defined as local functions, but are made available in the p
-- table for testing purposes.
----------------------------------------------------------------------------
local function message(cfgKey, valArray, expectType)
--[[
-- Gets a message from the cfg table and formats it if appropriate.
-- The function raises an error if the value from the cfg table is not
-- of the type expectType. The default type for expectType is 'string'.
-- If the table valArray is present, strings such as $1, $2 etc. in the
-- message are substituted with values from the table keys [1], [2] etc.
-- For example, if the message "foo-message" had the value 'Foo $2 bar $1.',
-- message('foo-message', {'baz', 'qux'}) would return "Foo qux bar baz."
--]]
local msg = cfg[cfgKey]
expectType = expectType or 'string'
if type(msg) ~= expectType then
error('message: type error in message cfg.' .. cfgKey .. ' (' .. expectType .. ' expected, got ' .. type(msg) .. ')', 2)
end
if not valArray then
return msg
end
local function getMessageVal(match)
match = tonumber(match)
return valArray[match] or error('message: no value found for key $' .. match .. ' in message cfg.' .. cfgKey, 4)
end
return ugsub(msg, '$([1-9][0-9]*)', getMessageVal)
end
p.message = message
local function makeWikilink(page, display)
if display then
return mw.ustring.format('[[%s|%s]]', page, display)
else
return mw.ustring.format('[[%s]]', page)
end
end
p.makeWikilink = makeWikilink
local function makeCategoryLink(cat, sort)
local catns = mw.site.namespaces[14].name
return makeWikilink(catns .. ':' .. cat, sort)
end
p.makeCategoryLink = makeCategoryLink
local function makeUrlLink(url, display)
return mw.ustring.format('[%s %s]', url, display)
end
p.makeUrlLink = makeUrlLink
local function makeToolbar(...)
local ret = {}
local lim = select('#', ...)
if lim < 1 then
return nil
end
for i = 1, lim do
ret[#ret + 1] = select(i, ...)
end
-- 'documentation-toolbar'
return '<span class="' .. message('toolbar-class') .. '">('
.. table.concat(ret, ' | ') .. ')</span>'
end
p.makeToolbar = makeToolbar
----------------------------------------------------------------------------
-- Argument processing
----------------------------------------------------------------------------
local function makeInvokeFunc(funcName)
return function (frame)
local args = getArgs(frame, {
valueFunc = function (key, value)
if type(value) == 'string' then
value = value:match('^%s*(.-)%s*$') -- Remove whitespace.
if key == 'heading' or value ~= '' then
return value
else
return nil
end
else
return value
end
end
})
return p[funcName](args)
end
end
----------------------------------------------------------------------------
-- Entry points
----------------------------------------------------------------------------
function p.nonexistent(frame)
if mw.title.getCurrentTitle().subpageText == 'testcases' then
return frame:expandTemplate{title = 'module test cases notice'}
else
return p.main(frame)
end
end
p.main = makeInvokeFunc('_main')
function p._main(args)
--[[
-- This function defines logic flow for the module.
-- @args - table of arguments passed by the user
--]]
local env = p.getEnvironment(args)
local root = mw.html.create()
root
:tag('div')
-- 'documentation-container'
:addClass(message('container'))
:attr('role', 'complementary')
:attr('aria-labelledby', args.heading ~= '' and 'documentation-heading' or nil)
:attr('aria-label', args.heading == '' and 'Documentation' or nil)
:newline()
:tag('div')
-- 'documentation'
:addClass(message('main-div-classes'))
:newline()
:wikitext(p._startBox(args, env))
:wikitext(p._content(args, env))
:tag('div')
-- 'documentation-clear'
:addClass(message('clear'))
:done()
:newline()
:done()
:wikitext(p._endBox(args, env))
:done()
:wikitext(p.addTrackingCategories(env))
-- 'Module:Documentation/styles.css'
return mw.getCurrentFrame():extensionTag (
'templatestyles', '', {src=cfg['templatestyles']
}) .. tostring(root)
end
----------------------------------------------------------------------------
-- Environment settings
----------------------------------------------------------------------------
function p.getEnvironment(args)
--[[
-- Returns a table with information about the environment, including title
-- objects and other namespace- or path-related data.
-- @args - table of arguments passed by the user
--
-- Title objects include:
-- env.title - the page we are making documentation for (usually the current title)
-- env.templateTitle - the template (or module, file, etc.)
-- env.docTitle - the /doc subpage.
-- env.sandboxTitle - the /sandbox subpage.
-- env.testcasesTitle - the /testcases subpage.
--
-- Data includes:
-- env.subjectSpace - the number of the title's subject namespace.
-- env.docSpace - the number of the namespace the title puts its documentation in.
-- env.docpageBase - the text of the base page of the /doc, /sandbox and /testcases pages, with namespace.
-- env.compareUrl - URL of the Special:ComparePages page comparing the sandbox with the template.
--
-- All table lookups are passed through pcall so that errors are caught. If an error occurs, the value
-- returned will be nil.
--]]
local env, envFuncs = {}, {}
-- Set up the metatable. If triggered we call the corresponding function in the envFuncs table. The value
-- returned by that function is memoized in the env table so that we don't call any of the functions
-- more than once. (Nils won't be memoized.)
setmetatable(env, {
__index = function (t, key)
local envFunc = envFuncs[key]
if envFunc then
local success, val = pcall(envFunc)
if success then
env[key] = val -- Memoise the value.
return val
end
end
return nil
end
})
function envFuncs.title()
-- The title object for the current page, or a test page passed with args.page.
local title
local titleArg = args.page
if titleArg then
title = mw.title.new(titleArg)
else
title = mw.title.getCurrentTitle()
end
return title
end
function envFuncs.templateTitle()
--[[
-- The template (or module, etc.) title object.
-- Messages:
-- 'sandbox-subpage' --> 'sandbox'
-- 'testcases-subpage' --> 'testcases'
--]]
local subjectSpace = env.subjectSpace
local title = env.title
local subpage = title.subpageText
if subpage == message('sandbox-subpage') or subpage == message('testcases-subpage') then
return mw.title.makeTitle(subjectSpace, title.baseText)
else
return mw.title.makeTitle(subjectSpace, title.text)
end
end
function envFuncs.docTitle()
--[[
-- Title object of the /doc subpage.
-- Messages:
-- 'doc-subpage' --> 'doc'
--]]
local title = env.title
local docname = args[1] -- User-specified doc page.
local docpage
if docname then
docpage = docname
else
docpage = env.docpageBase .. '/' .. message('doc-subpage')
end
return mw.title.new(docpage)
end
function envFuncs.sandboxTitle()
--[[
-- Title object for the /sandbox subpage.
-- Messages:
-- 'sandbox-subpage' --> 'sandbox'
--]]
return mw.title.new(env.docpageBase .. '/' .. message('sandbox-subpage'))
end
function envFuncs.testcasesTitle()
--[[
-- Title object for the /testcases subpage.
-- Messages:
-- 'testcases-subpage' --> 'testcases'
--]]
return mw.title.new(env.docpageBase .. '/' .. message('testcases-subpage'))
end
function envFuncs.subjectSpace()
-- The subject namespace number.
return mw.site.namespaces[env.title.namespace].subject.id
end
function envFuncs.docSpace()
-- The documentation namespace number. For most namespaces this is the
-- same as the subject namespace. However, pages in the Article, File,
-- MediaWiki or Category namespaces must have their /doc, /sandbox and
-- /testcases pages in talk space.
local subjectSpace = env.subjectSpace
if subjectSpace == 0 or subjectSpace == 6 or subjectSpace == 8 or subjectSpace == 14 then
return subjectSpace + 1
else
return subjectSpace
end
end
function envFuncs.docpageBase()
-- The base page of the /doc, /sandbox, and /testcases subpages.
-- For some namespaces this is the talk page, rather than the template page.
local templateTitle = env.templateTitle
local docSpace = env.docSpace
local docSpaceText = mw.site.namespaces[docSpace].name
-- Assemble the link. docSpace is never the main namespace, so we can hardcode the colon.
return docSpaceText .. ':' .. templateTitle.text
end
function envFuncs.compareUrl()
-- Diff link between the sandbox and the main template using [[Special:ComparePages]].
local templateTitle = env.templateTitle
local sandboxTitle = env.sandboxTitle
if templateTitle.exists and sandboxTitle.exists then
local compareUrl = mw.uri.fullUrl(
'Special:ComparePages',
{ page1 = templateTitle.prefixedText, page2 = sandboxTitle.prefixedText}
)
return tostring(compareUrl)
else
return nil
end
end
return env
end
----------------------------------------------------------------------------
-- Start box
----------------------------------------------------------------------------
p.startBox = makeInvokeFunc('_startBox')
function p._startBox(args, env)
--[[
-- This function generates the start box.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- The actual work is done by p.makeStartBoxLinksData and p.renderStartBoxLinks which make
-- the [view] [edit] [history] [purge] links, and by p.makeStartBoxData and p.renderStartBox
-- which generate the box HTML.
--]]
env = env or p.getEnvironment(args)
local links
local content = args.content
if not content or args[1] then
-- No need to include the links if the documentation is on the template page itself.
local linksData = p.makeStartBoxLinksData(args, env)
if linksData then
links = p.renderStartBoxLinks(linksData)
end
end
-- Generate the start box html.
local data = p.makeStartBoxData(args, env, links)
if data then
return p.renderStartBox(data)
else
-- User specified no heading.
return nil
end
end
function p.makeStartBoxLinksData(args, env)
--[[
-- Does initial processing of data to make the [view] [edit] [history] [purge] links.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'view-link-display' --> 'view'
-- 'edit-link-display' --> 'edit'
-- 'history-link-display' --> 'history'
-- 'purge-link-display' --> 'purge'
-- 'module-preload' --> 'Template:Documentation/preload-module-doc'
-- 'docpage-preload' --> 'Template:Documentation/preload'
-- 'create-link-display' --> 'create'
--]]
local subjectSpace = env.subjectSpace
local title = env.title
local docTitle = env.docTitle
if not title or not docTitle then
return nil
end
if docTitle.isRedirect then
docTitle = docTitle.redirectTarget
end
local data = {}
data.title = title
data.docTitle = docTitle
-- View, display, edit, and purge links if /doc exists.
data.viewLinkDisplay = message('view-link-display')
data.editLinkDisplay = message('edit-link-display')
data.historyLinkDisplay = message('history-link-display')
data.purgeLinkDisplay = message('purge-link-display')
-- Create link if /doc doesn't exist.
local preload = args.preload
if not preload then
if subjectSpace == 828 then -- Module namespace
preload = message('module-preload')
else
preload = message('docpage-preload')
end
end
data.preload = preload
data.createLinkDisplay = message('create-link-display')
return data
end
function p.renderStartBoxLinks(data)
--[[
-- Generates the [view][edit][history][purge] or [create][purge] links from the data table.
-- @data - a table of data generated by p.makeStartBoxLinksData
--]]
local function escapeBrackets(s)
-- Escapes square brackets with HTML entities.
s = s:gsub('%[', '[') -- Replace square brackets with HTML entities.
s = s:gsub('%]', ']')
return s
end
local ret
local docTitle = data.docTitle
local title = data.title
local purgeLink = makeUrlLink(title:fullUrl{action = 'purge'}, data.purgeLinkDisplay)
if docTitle.exists then
local viewLink = makeWikilink(docTitle.prefixedText, data.viewLinkDisplay)
local editLink = makeUrlLink(docTitle:fullUrl{action = 'edit'}, data.editLinkDisplay)
local historyLink = makeUrlLink(docTitle:fullUrl{action = 'history'}, data.historyLinkDisplay)
ret = '[%s] [%s] [%s] [%s]'
ret = escapeBrackets(ret)
ret = mw.ustring.format(ret, viewLink, editLink, historyLink, purgeLink)
else
local createLink = makeUrlLink(docTitle:fullUrl{action = 'edit', preload = data.preload}, data.createLinkDisplay)
ret = '[%s] [%s]'
ret = escapeBrackets(ret)
ret = mw.ustring.format(ret, createLink, purgeLink)
end
return ret
end
function p.makeStartBoxData(args, env, links)
--[=[
-- Does initial processing of data to pass to the start-box render function, p.renderStartBox.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- @links - a string containing the [view][edit][history][purge] links - could be nil if there's an error.
--
-- Messages:
-- 'documentation-icon-wikitext' --> '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]'
-- 'template-namespace-heading' --> 'Template documentation'
-- 'module-namespace-heading' --> 'Module documentation'
-- 'file-namespace-heading' --> 'Summary'
-- 'other-namespaces-heading' --> 'Documentation'
-- 'testcases-create-link-display' --> 'create'
--]=]
local subjectSpace = env.subjectSpace
if not subjectSpace then
-- Default to an "other namespaces" namespace, so that we get at least some output
-- if an error occurs.
subjectSpace = 2
end
local data = {}
-- Heading
local heading = args.heading -- Blank values are not removed.
if heading == '' then
-- Don't display the start box if the heading arg is defined but blank.
return nil
end
if heading then
data.heading = heading
elseif subjectSpace == 10 then -- Template namespace
data.heading = message('documentation-icon-wikitext') .. ' ' .. message('template-namespace-heading')
elseif subjectSpace == 828 then -- Module namespace
data.heading = message('documentation-icon-wikitext') .. ' ' .. message('module-namespace-heading')
elseif subjectSpace == 6 then -- File namespace
data.heading = message('file-namespace-heading')
else
data.heading = message('other-namespaces-heading')
end
-- Heading CSS
local headingStyle = args['heading-style']
if headingStyle then
data.headingStyleText = headingStyle
else
-- 'documentation-heading'
data.headingClass = message('main-div-heading-class')
end
-- Data for the [view][edit][history][purge] or [create] links.
if links then
-- 'mw-editsection-like plainlinks'
data.linksClass = message('start-box-link-classes')
data.links = links
end
return data
end
function p.renderStartBox(data)
-- Renders the start box html.
-- @data - a table of data generated by p.makeStartBoxData.
local sbox = mw.html.create('div')
sbox
-- 'documentation-startbox'
:addClass(message('start-box-class'))
:newline()
:tag('span')
:addClass(data.headingClass)
:attr('id', 'documentation-heading')
:cssText(data.headingStyleText)
:wikitext(data.heading)
local links = data.links
if links then
sbox:tag('span')
:addClass(data.linksClass)
:attr('id', data.linksId)
:wikitext(links)
end
return tostring(sbox)
end
----------------------------------------------------------------------------
-- Documentation content
----------------------------------------------------------------------------
p.content = makeInvokeFunc('_content')
function p._content(args, env)
-- Displays the documentation contents
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
env = env or p.getEnvironment(args)
local docTitle = env.docTitle
local content = args.content
if not content and docTitle and docTitle.exists then
content = args._content or mw.getCurrentFrame():expandTemplate{title = docTitle.prefixedText}
end
-- The line breaks below are necessary so that "=== Headings ===" at the start and end
-- of docs are interpreted correctly.
return '\n' .. (content or '') .. '\n'
end
p.contentTitle = makeInvokeFunc('_contentTitle')
function p._contentTitle(args, env)
env = env or p.getEnvironment(args)
local docTitle = env.docTitle
if not args.content and docTitle and docTitle.exists then
return docTitle.prefixedText
else
return ''
end
end
----------------------------------------------------------------------------
-- End box
----------------------------------------------------------------------------
p.endBox = makeInvokeFunc('_endBox')
function p._endBox(args, env)
--[=[
-- This function generates the end box (also known as the link box).
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
--]=]
-- Get environment data.
env = env or p.getEnvironment(args)
local subjectSpace = env.subjectSpace
local docTitle = env.docTitle
if not subjectSpace or not docTitle then
return nil
end
-- Check whether we should output the end box at all. Add the end
-- box by default if the documentation exists or if we are in the
-- user, module or template namespaces.
local linkBox = args['link box']
if linkBox == 'off'
or not (
docTitle.exists
or subjectSpace == 2
or subjectSpace == 828
or subjectSpace == 10
)
then
return nil
end
-- Assemble the link box.
local text = ''
if linkBox then
text = text .. linkBox
else
text = text .. (p.makeDocPageBlurb(args, env) or '') -- "This documentation is transcluded from [[Foo]]."
if subjectSpace == 2 or subjectSpace == 10 or subjectSpace == 828 then
-- We are in the user, template or module namespaces.
-- Add sandbox and testcases links.
-- "Editors can experiment in this template's sandbox and testcases pages."
text = text .. (p.makeExperimentBlurb(args, env) or '') .. '<br />'
if not args.content and not args[1] then
-- "Please add categories to the /doc subpage."
-- Don't show this message with inline docs or with an explicitly specified doc page,
-- as then it is unclear where to add the categories.
text = text .. (p.makeCategoriesBlurb(args, env) or '')
end
text = text .. ' ' .. (p.makeSubpagesBlurb(args, env) or '') --"Subpages of this template"
end
end
local box = mw.html.create('div')
-- 'documentation-metadata'
box:attr('role', 'note')
:addClass(message('end-box-class'))
-- 'plainlinks'
:addClass(message('end-box-plainlinks'))
:wikitext(text)
:done()
return '\n' .. tostring(box)
end
function p.makeDocPageBlurb(args, env)
--[=[
-- Makes the blurb "This documentation is transcluded from [[Template:Foo]] (edit, history)".
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'edit-link-display' --> 'edit'
-- 'history-link-display' --> 'history'
-- 'transcluded-from-blurb' -->
-- 'The above [[Wikipedia:Template documentation|documentation]]
-- is [[Help:Transclusion|transcluded]] from $1.'
-- 'module-preload' --> 'Template:Documentation/preload-module-doc'
-- 'create-link-display' --> 'create'
-- 'create-module-doc-blurb' -->
-- 'You might want to $1 a documentation page for this [[Wikipedia:Lua|Scribunto module]].'
--]=]
local docTitle = env.docTitle
if not docTitle then
return nil
end
local ret
if docTitle.exists then
-- /doc exists; link to it.
local docLink = makeWikilink(docTitle.prefixedText)
local editUrl = docTitle:fullUrl{action = 'edit'}
local editDisplay = message('edit-link-display')
local editLink = makeUrlLink(editUrl, editDisplay)
local historyUrl = docTitle:fullUrl{action = 'history'}
local historyDisplay = message('history-link-display')
local historyLink = makeUrlLink(historyUrl, historyDisplay)
ret = message('transcluded-from-blurb', {docLink})
.. ' '
.. makeToolbar(editLink, historyLink)
.. '<br />'
elseif env.subjectSpace == 828 then
-- /doc does not exist; ask to create it.
local createUrl = docTitle:fullUrl{action = 'edit', preload = message('module-preload')}
local createDisplay = message('create-link-display')
local createLink = makeUrlLink(createUrl, createDisplay)
ret = message('create-module-doc-blurb', {createLink})
.. '<br />'
end
return ret
end
function p.makeExperimentBlurb(args, env)
--[[
-- Renders the text "Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages."
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
--
-- Messages:
-- 'sandbox-link-display' --> 'sandbox'
-- 'sandbox-edit-link-display' --> 'edit'
-- 'compare-link-display' --> 'diff'
-- 'module-sandbox-preload' --> 'Template:Documentation/preload-module-sandbox'
-- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- 'sandbox-create-link-display' --> 'create'
-- 'mirror-edit-summary' --> 'Create sandbox version of $1'
-- 'mirror-link-display' --> 'mirror'
-- 'mirror-link-preload' --> 'Template:Documentation/mirror'
-- 'sandbox-link-display' --> 'sandbox'
-- 'testcases-link-display' --> 'testcases'
-- 'testcases-edit-link-display'--> 'edit'
-- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox'
-- 'testcases-create-link-display' --> 'create'
-- 'testcases-link-display' --> 'testcases'
-- 'testcases-edit-link-display' --> 'edit'
-- 'module-testcases-preload' --> 'Template:Documentation/preload-module-testcases'
-- 'template-testcases-preload' --> 'Template:Documentation/preload-testcases'
-- 'experiment-blurb-module' --> 'Editors can experiment in this module's $1 and $2 pages.'
-- 'experiment-blurb-template' --> 'Editors can experiment in this template's $1 and $2 pages.'
--]]
local subjectSpace = env.subjectSpace
local templateTitle = env.templateTitle
local sandboxTitle = env.sandboxTitle
local testcasesTitle = env.testcasesTitle
local templatePage = templateTitle.prefixedText
if not subjectSpace or not templateTitle or not sandboxTitle or not testcasesTitle then
return nil
end
-- Make links.
local sandboxLinks, testcasesLinks
if sandboxTitle.exists then
local sandboxPage = sandboxTitle.prefixedText
local sandboxDisplay = message('sandbox-link-display')
local sandboxLink = makeWikilink(sandboxPage, sandboxDisplay)
local sandboxEditUrl = sandboxTitle:fullUrl{action = 'edit'}
local sandboxEditDisplay = message('sandbox-edit-link-display')
local sandboxEditLink = makeUrlLink(sandboxEditUrl, sandboxEditDisplay)
local compareUrl = env.compareUrl
local compareLink
if compareUrl then
local compareDisplay = message('compare-link-display')
compareLink = makeUrlLink(compareUrl, compareDisplay)
end
sandboxLinks = sandboxLink .. ' ' .. makeToolbar(sandboxEditLink, compareLink)
else
local sandboxPreload
if subjectSpace == 828 then
sandboxPreload = message('module-sandbox-preload')
else
sandboxPreload = message('template-sandbox-preload')
end
local sandboxCreateUrl = sandboxTitle:fullUrl{action = 'edit', preload = sandboxPreload}
local sandboxCreateDisplay = message('sandbox-create-link-display')
local sandboxCreateLink = makeUrlLink(sandboxCreateUrl, sandboxCreateDisplay)
local mirrorSummary = message('mirror-edit-summary', {makeWikilink(templatePage)})
local mirrorPreload = message('mirror-link-preload')
local mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = mirrorPreload, summary = mirrorSummary}
if subjectSpace == 828 then
mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = templateTitle.prefixedText, summary = mirrorSummary}
end
local mirrorDisplay = message('mirror-link-display')
local mirrorLink = makeUrlLink(mirrorUrl, mirrorDisplay)
sandboxLinks = message('sandbox-link-display') .. ' ' .. makeToolbar(sandboxCreateLink, mirrorLink)
end
if testcasesTitle.exists then
local testcasesPage = testcasesTitle.prefixedText
local testcasesDisplay = message('testcases-link-display')
local testcasesLink = makeWikilink(testcasesPage, testcasesDisplay)
local testcasesEditUrl = testcasesTitle:fullUrl{action = 'edit'}
local testcasesEditDisplay = message('testcases-edit-link-display')
local testcasesEditLink = makeUrlLink(testcasesEditUrl, testcasesEditDisplay)
-- for Modules, add testcases run link if exists
if testcasesTitle.contentModel == "Scribunto" and testcasesTitle.talkPageTitle and testcasesTitle.talkPageTitle.exists then
local testcasesRunLinkDisplay = message('testcases-run-link-display')
local testcasesRunLink = makeWikilink(testcasesTitle.talkPageTitle.prefixedText, testcasesRunLinkDisplay)
testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink, testcasesRunLink)
else
testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink)
end
else
local testcasesPreload
if subjectSpace == 828 then
testcasesPreload = message('module-testcases-preload')
else
testcasesPreload = message('template-testcases-preload')
end
local testcasesCreateUrl = testcasesTitle:fullUrl{action = 'edit', preload = testcasesPreload}
local testcasesCreateDisplay = message('testcases-create-link-display')
local testcasesCreateLink = makeUrlLink(testcasesCreateUrl, testcasesCreateDisplay)
testcasesLinks = message('testcases-link-display') .. ' ' .. makeToolbar(testcasesCreateLink)
end
local messageName
if subjectSpace == 828 then
messageName = 'experiment-blurb-module'
else
messageName = 'experiment-blurb-template'
end
return message(messageName, {sandboxLinks, testcasesLinks})
end
function p.makeCategoriesBlurb(args, env)
--[[
-- Generates the text "Please add categories to the /doc subpage."
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'doc-link-display' --> '/doc'
-- 'add-categories-blurb' --> 'Please add categories to the $1 subpage.'
--]]
local docTitle = env.docTitle
if not docTitle then
return nil
end
local docPathLink = makeWikilink(docTitle.prefixedText, message('doc-link-display'))
return message('add-categories-blurb', {docPathLink})
end
function p.makeSubpagesBlurb(args, env)
--[[
-- Generates the "Subpages of this template" link.
-- @args - a table of arguments passed by the user
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'template-pagetype' --> 'template'
-- 'module-pagetype' --> 'module'
-- 'default-pagetype' --> 'page'
-- 'subpages-link-display' --> 'Subpages of this $1'
--]]
local subjectSpace = env.subjectSpace
local templateTitle = env.templateTitle
if not subjectSpace or not templateTitle then
return nil
end
local pagetype
if subjectSpace == 10 then
pagetype = message('template-pagetype')
elseif subjectSpace == 828 then
pagetype = message('module-pagetype')
else
pagetype = message('default-pagetype')
end
local subpagesLink = makeWikilink(
'Special:PrefixIndex/' .. templateTitle.prefixedText .. '/',
message('subpages-link-display', {pagetype})
)
return message('subpages-blurb', {subpagesLink})
end
----------------------------------------------------------------------------
-- Tracking categories
----------------------------------------------------------------------------
function p.addTrackingCategories(env)
--[[
-- Check if {{documentation}} is transcluded on a /doc or /testcases page.
-- @env - environment table containing title objects, etc., generated with p.getEnvironment
-- Messages:
-- 'display-strange-usage-category' --> true
-- 'doc-subpage' --> 'doc'
-- 'testcases-subpage' --> 'testcases'
-- 'strange-usage-category' --> 'Wikipedia pages with strange ((documentation)) usage'
--
-- /testcases pages in the module namespace are not categorised, as they may have
-- {{documentation}} transcluded automatically.
--]]
local title = env.title
local subjectSpace = env.subjectSpace
if not title or not subjectSpace then
return nil
end
local subpage = title.subpageText
local ret = ''
if message('display-strange-usage-category', nil, 'boolean')
and (
subpage == message('doc-subpage')
or subjectSpace ~= 828 and subpage == message('testcases-subpage')
)
then
ret = ret .. makeCategoryLink(message('strange-usage-category'))
end
return ret
end
return p
78cc3a78f2b5dbb267fa16027c0800a22dbd3c42
Module:Arguments
828
21
36
35
2023-08-27T19:29:38Z
Alxira5
4
1 revision imported: Module imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
Scribunto
text/plain
-- This module provides easy processing of arguments passed to Scribunto from
-- #invoke. It is intended for use by other Lua modules, and should not be
-- called from #invoke directly.
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local arguments = {}
-- Generate four different tidyVal functions, so that we don't have to check the
-- options every time we call it.
local function tidyValDefault(key, val)
if type(val) == 'string' then
val = val:match('^%s*(.-)%s*$')
if val == '' then
return nil
else
return val
end
else
return val
end
end
local function tidyValTrimOnly(key, val)
if type(val) == 'string' then
return val:match('^%s*(.-)%s*$')
else
return val
end
end
local function tidyValRemoveBlanksOnly(key, val)
if type(val) == 'string' then
if val:find('%S') then
return val
else
return nil
end
else
return val
end
end
local function tidyValNoChange(key, val)
return val
end
local function matchesTitle(given, title)
local tp = type( given )
return (tp == 'string' or tp == 'number') and mw.title.new( given ).prefixedText == title
end
local translate_mt = { __index = function(t, k) return k end }
function arguments.getArgs(frame, options)
checkType('getArgs', 1, frame, 'table', true)
checkType('getArgs', 2, options, 'table', true)
frame = frame or {}
options = options or {}
--[[
-- Set up argument translation.
--]]
options.translate = options.translate or {}
if getmetatable(options.translate) == nil then
setmetatable(options.translate, translate_mt)
end
if options.backtranslate == nil then
options.backtranslate = {}
for k,v in pairs(options.translate) do
options.backtranslate[v] = k
end
end
if options.backtranslate and getmetatable(options.backtranslate) == nil then
setmetatable(options.backtranslate, {
__index = function(t, k)
if options.translate[k] ~= k then
return nil
else
return k
end
end
})
end
--[[
-- Get the argument tables. If we were passed a valid frame object, get the
-- frame arguments (fargs) and the parent frame arguments (pargs), depending
-- on the options set and on the parent frame's availability. If we weren't
-- passed a valid frame object, we are being called from another Lua module
-- or from the debug console, so assume that we were passed a table of args
-- directly, and assign it to a new variable (luaArgs).
--]]
local fargs, pargs, luaArgs
if type(frame.args) == 'table' and type(frame.getParent) == 'function' then
if options.wrappers then
--[[
-- The wrappers option makes Module:Arguments look up arguments in
-- either the frame argument table or the parent argument table, but
-- not both. This means that users can use either the #invoke syntax
-- or a wrapper template without the loss of performance associated
-- with looking arguments up in both the frame and the parent frame.
-- Module:Arguments will look up arguments in the parent frame
-- if it finds the parent frame's title in options.wrapper;
-- otherwise it will look up arguments in the frame object passed
-- to getArgs.
--]]
local parent = frame:getParent()
if not parent then
fargs = frame.args
else
local title = parent:getTitle():gsub('/sandbox$', '')
local found = false
if matchesTitle(options.wrappers, title) then
found = true
elseif type(options.wrappers) == 'table' then
for _,v in pairs(options.wrappers) do
if matchesTitle(v, title) then
found = true
break
end
end
end
-- We test for false specifically here so that nil (the default) acts like true.
if found or options.frameOnly == false then
pargs = parent.args
end
if not found or options.parentOnly == false then
fargs = frame.args
end
end
else
-- options.wrapper isn't set, so check the other options.
if not options.parentOnly then
fargs = frame.args
end
if not options.frameOnly then
local parent = frame:getParent()
pargs = parent and parent.args or nil
end
end
if options.parentFirst then
fargs, pargs = pargs, fargs
end
else
luaArgs = frame
end
-- Set the order of precedence of the argument tables. If the variables are
-- nil, nothing will be added to the table, which is how we avoid clashes
-- between the frame/parent args and the Lua args.
local argTables = {fargs}
argTables[#argTables + 1] = pargs
argTables[#argTables + 1] = luaArgs
--[[
-- Generate the tidyVal function. If it has been specified by the user, we
-- use that; if not, we choose one of four functions depending on the
-- options chosen. This is so that we don't have to call the options table
-- every time the function is called.
--]]
local tidyVal = options.valueFunc
if tidyVal then
if type(tidyVal) ~= 'function' then
error(
"bad value assigned to option 'valueFunc'"
.. '(function expected, got '
.. type(tidyVal)
.. ')',
2
)
end
elseif options.trim ~= false then
if options.removeBlanks ~= false then
tidyVal = tidyValDefault
else
tidyVal = tidyValTrimOnly
end
else
if options.removeBlanks ~= false then
tidyVal = tidyValRemoveBlanksOnly
else
tidyVal = tidyValNoChange
end
end
--[[
-- Set up the args, metaArgs and nilArgs tables. args will be the one
-- accessed from functions, and metaArgs will hold the actual arguments. Nil
-- arguments are memoized in nilArgs, and the metatable connects all of them
-- together.
--]]
local args, metaArgs, nilArgs, metatable = {}, {}, {}, {}
setmetatable(args, metatable)
local function mergeArgs(tables)
--[[
-- Accepts multiple tables as input and merges their keys and values
-- into one table. If a value is already present it is not overwritten;
-- tables listed earlier have precedence. We are also memoizing nil
-- values, which can be overwritten if they are 's' (soft).
--]]
for _, t in ipairs(tables) do
for key, val in pairs(t) do
if metaArgs[key] == nil and nilArgs[key] ~= 'h' then
local tidiedVal = tidyVal(key, val)
if tidiedVal == nil then
nilArgs[key] = 's'
else
metaArgs[key] = tidiedVal
end
end
end
end
end
--[[
-- Define metatable behaviour. Arguments are memoized in the metaArgs table,
-- and are only fetched from the argument tables once. Fetching arguments
-- from the argument tables is the most resource-intensive step in this
-- module, so we try and avoid it where possible. For this reason, nil
-- arguments are also memoized, in the nilArgs table. Also, we keep a record
-- in the metatable of when pairs and ipairs have been called, so we do not
-- run pairs and ipairs on the argument tables more than once. We also do
-- not run ipairs on fargs and pargs if pairs has already been run, as all
-- the arguments will already have been copied over.
--]]
metatable.__index = function (t, key)
--[[
-- Fetches an argument when the args table is indexed. First we check
-- to see if the value is memoized, and if not we try and fetch it from
-- the argument tables. When we check memoization, we need to check
-- metaArgs before nilArgs, as both can be non-nil at the same time.
-- If the argument is not present in metaArgs, we also check whether
-- pairs has been run yet. If pairs has already been run, we return nil.
-- This is because all the arguments will have already been copied into
-- metaArgs by the mergeArgs function, meaning that any other arguments
-- must be nil.
--]]
if type(key) == 'string' then
key = options.translate[key]
end
local val = metaArgs[key]
if val ~= nil then
return val
elseif metatable.donePairs or nilArgs[key] then
return nil
end
for _, argTable in ipairs(argTables) do
local argTableVal = tidyVal(key, argTable[key])
if argTableVal ~= nil then
metaArgs[key] = argTableVal
return argTableVal
end
end
nilArgs[key] = 'h'
return nil
end
metatable.__newindex = function (t, key, val)
-- This function is called when a module tries to add a new value to the
-- args table, or tries to change an existing value.
if type(key) == 'string' then
key = options.translate[key]
end
if options.readOnly then
error(
'could not write to argument table key "'
.. tostring(key)
.. '"; the table is read-only',
2
)
elseif options.noOverwrite and args[key] ~= nil then
error(
'could not write to argument table key "'
.. tostring(key)
.. '"; overwriting existing arguments is not permitted',
2
)
elseif val == nil then
--[[
-- If the argument is to be overwritten with nil, we need to erase
-- the value in metaArgs, so that __index, __pairs and __ipairs do
-- not use a previous existing value, if present; and we also need
-- to memoize the nil in nilArgs, so that the value isn't looked
-- up in the argument tables if it is accessed again.
--]]
metaArgs[key] = nil
nilArgs[key] = 'h'
else
metaArgs[key] = val
end
end
local function translatenext(invariant)
local k, v = next(invariant.t, invariant.k)
invariant.k = k
if k == nil then
return nil
elseif type(k) ~= 'string' or not options.backtranslate then
return k, v
else
local backtranslate = options.backtranslate[k]
if backtranslate == nil then
-- Skip this one. This is a tail call, so this won't cause stack overflow
return translatenext(invariant)
else
return backtranslate, v
end
end
end
metatable.__pairs = function ()
-- Called when pairs is run on the args table.
if not metatable.donePairs then
mergeArgs(argTables)
metatable.donePairs = true
end
return translatenext, { t = metaArgs }
end
local function inext(t, i)
-- This uses our __index metamethod
local v = t[i + 1]
if v ~= nil then
return i + 1, v
end
end
metatable.__ipairs = function (t)
-- Called when ipairs is run on the args table.
return inext, t, 0
end
return args
end
return arguments
3134ecce8429b810d445e29eae115e2ae4c36c53
Module:Documentation/config
828
22
38
37
2023-08-27T20:01:04Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
Scribunto
text/plain
----------------------------------------------------------------------------------------------------
--
-- Configuration for Module:Documentation
--
-- Here you can set the values of the parameters and messages used in Module:Documentation to
-- localise it to your wiki and your language. Unless specified otherwise, values given here
-- should be string values.
----------------------------------------------------------------------------------------------------
local cfg = {} -- Do not edit this line.
----------------------------------------------------------------------------------------------------
-- Start box configuration
----------------------------------------------------------------------------------------------------
-- cfg['documentation-icon-wikitext']
-- The wikitext for the icon shown at the top of the template.
cfg['documentation-icon-wikitext'] = '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]'
-- cfg['template-namespace-heading']
-- The heading shown in the template namespace.
cfg['template-namespace-heading'] = 'Template documentation'
-- cfg['module-namespace-heading']
-- The heading shown in the module namespace.
cfg['module-namespace-heading'] = 'Module documentation'
-- cfg['file-namespace-heading']
-- The heading shown in the file namespace.
cfg['file-namespace-heading'] = 'Summary'
-- cfg['other-namespaces-heading']
-- The heading shown in other namespaces.
cfg['other-namespaces-heading'] = 'Documentation'
-- cfg['view-link-display']
-- The text to display for "view" links.
cfg['view-link-display'] = 'view'
-- cfg['edit-link-display']
-- The text to display for "edit" links.
cfg['edit-link-display'] = 'edit'
-- cfg['history-link-display']
-- The text to display for "history" links.
cfg['history-link-display'] = 'history'
-- cfg['purge-link-display']
-- The text to display for "purge" links.
cfg['purge-link-display'] = 'purge'
-- cfg['create-link-display']
-- The text to display for "create" links.
cfg['create-link-display'] = 'create'
----------------------------------------------------------------------------------------------------
-- Link box (end box) configuration
----------------------------------------------------------------------------------------------------
-- cfg['transcluded-from-blurb']
-- Notice displayed when the docs are transcluded from another page. $1 is a wikilink to that page.
cfg['transcluded-from-blurb'] = 'The above [[w:Wikipedia:Template documentation|documentation]] is [[mw:Help:Transclusion|transcluded]] from $1.'
--[[
-- cfg['create-module-doc-blurb']
-- Notice displayed in the module namespace when the documentation subpage does not exist.
-- $1 is a link to create the documentation page with the preload cfg['module-preload'] and the
-- display cfg['create-link-display'].
--]]
cfg['create-module-doc-blurb'] = 'You might want to $1 a documentation page for this [[mw:Extension:Scribunto/Lua reference manual|Scribunto module]].'
----------------------------------------------------------------------------------------------------
-- Experiment blurb configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['experiment-blurb-template']
-- cfg['experiment-blurb-module']
-- The experiment blurb is the text inviting editors to experiment in sandbox and test cases pages.
-- It is only shown in the template and module namespaces. With the default English settings, it
-- might look like this:
--
-- Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages.
--
-- In this example, "sandbox", "edit", "diff", "testcases", and "edit" would all be links.
--
-- There are two versions, cfg['experiment-blurb-template'] and cfg['experiment-blurb-module'], depending
-- on what namespace we are in.
--
-- Parameters:
--
-- $1 is a link to the sandbox page. If the sandbox exists, it is in the following format:
--
-- cfg['sandbox-link-display'] (cfg['sandbox-edit-link-display'] | cfg['compare-link-display'])
--
-- If the sandbox doesn't exist, it is in the format:
--
-- cfg['sandbox-link-display'] (cfg['sandbox-create-link-display'] | cfg['mirror-link-display'])
--
-- The link for cfg['sandbox-create-link-display'] link preloads the page with cfg['template-sandbox-preload']
-- or cfg['module-sandbox-preload'], depending on the current namespace. The link for cfg['mirror-link-display']
-- loads a default edit summary of cfg['mirror-edit-summary'].
--
-- $2 is a link to the test cases page. If the test cases page exists, it is in the following format:
--
-- cfg['testcases-link-display'] (cfg['testcases-edit-link-display'] | cfg['testcases-run-link-display'])
--
-- If the test cases page doesn't exist, it is in the format:
--
-- cfg['testcases-link-display'] (cfg['testcases-create-link-display'])
--
-- If the test cases page doesn't exist, the link for cfg['testcases-create-link-display'] preloads the
-- page with cfg['template-testcases-preload'] or cfg['module-testcases-preload'], depending on the current
-- namespace.
--]]
cfg['experiment-blurb-template'] = "Editors can experiment in this template's $1 and $2 pages."
cfg['experiment-blurb-module'] = "Editors can experiment in this module's $1 and $2 pages."
----------------------------------------------------------------------------------------------------
-- Sandbox link configuration
----------------------------------------------------------------------------------------------------
-- cfg['sandbox-subpage']
-- The name of the template subpage typically used for sandboxes.
cfg['sandbox-subpage'] = 'sandbox'
-- cfg['template-sandbox-preload']
-- Preload file for template sandbox pages.
cfg['template-sandbox-preload'] = 'Template:Documentation/preload-sandbox'
-- cfg['module-sandbox-preload']
-- Preload file for Lua module sandbox pages.
cfg['module-sandbox-preload'] = 'Template:Documentation/preload-module-sandbox'
-- cfg['sandbox-link-display']
-- The text to display for "sandbox" links.
cfg['sandbox-link-display'] = 'sandbox'
-- cfg['sandbox-edit-link-display']
-- The text to display for sandbox "edit" links.
cfg['sandbox-edit-link-display'] = 'edit'
-- cfg['sandbox-create-link-display']
-- The text to display for sandbox "create" links.
cfg['sandbox-create-link-display'] = 'create'
-- cfg['compare-link-display']
-- The text to display for "compare" links.
cfg['compare-link-display'] = 'diff'
-- cfg['mirror-edit-summary']
-- The default edit summary to use when a user clicks the "mirror" link. $1 is a wikilink to the
-- template page.
cfg['mirror-edit-summary'] = 'Create sandbox version of $1'
-- cfg['mirror-link-display']
-- The text to display for "mirror" links.
cfg['mirror-link-display'] = 'mirror'
-- cfg['mirror-link-preload']
-- The page to preload when a user clicks the "mirror" link.
cfg['mirror-link-preload'] = 'Template:Documentation/mirror'
----------------------------------------------------------------------------------------------------
-- Test cases link configuration
----------------------------------------------------------------------------------------------------
-- cfg['testcases-subpage']
-- The name of the template subpage typically used for test cases.
cfg['testcases-subpage'] = 'testcases'
-- cfg['template-testcases-preload']
-- Preload file for template test cases pages.
cfg['template-testcases-preload'] = 'Template:Documentation/preload-testcases'
-- cfg['module-testcases-preload']
-- Preload file for Lua module test cases pages.
cfg['module-testcases-preload'] = 'Template:Documentation/preload-module-testcases'
-- cfg['testcases-link-display']
-- The text to display for "testcases" links.
cfg['testcases-link-display'] = 'testcases'
-- cfg['testcases-edit-link-display']
-- The text to display for test cases "edit" links.
cfg['testcases-edit-link-display'] = 'edit'
-- cfg['testcases-run-link-display']
-- The text to display for test cases "run" links.
cfg['testcases-run-link-display'] = 'run'
-- cfg['testcases-create-link-display']
-- The text to display for test cases "create" links.
cfg['testcases-create-link-display'] = 'create'
----------------------------------------------------------------------------------------------------
-- Add categories blurb configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['add-categories-blurb']
-- Text to direct users to add categories to the /doc subpage. Not used if the "content" or
-- "docname fed" arguments are set, as then it is not clear where to add the categories. $1 is a
-- link to the /doc subpage with a display value of cfg['doc-link-display'].
--]]
cfg['add-categories-blurb'] = 'Add categories to the $1 subpage.'
-- cfg['doc-link-display']
-- The text to display when linking to the /doc subpage.
cfg['doc-link-display'] = '/doc'
----------------------------------------------------------------------------------------------------
-- Subpages link configuration
----------------------------------------------------------------------------------------------------
--[[
-- cfg['subpages-blurb']
-- The "Subpages of this template" blurb. $1 is a link to the main template's subpages with a
-- display value of cfg['subpages-link-display']. In the English version this blurb is simply
-- the link followed by a period, and the link display provides the actual text.
--]]
cfg['subpages-blurb'] = '$1.'
--[[
-- cfg['subpages-link-display']
-- The text to display for the "subpages of this page" link. $1 is cfg['template-pagetype'],
-- cfg['module-pagetype'] or cfg['default-pagetype'], depending on whether the current page is in
-- the template namespace, the module namespace, or another namespace.
--]]
cfg['subpages-link-display'] = 'Subpages of this $1'
-- cfg['template-pagetype']
-- The pagetype to display for template pages.
cfg['template-pagetype'] = 'template'
-- cfg['module-pagetype']
-- The pagetype to display for Lua module pages.
cfg['module-pagetype'] = 'module'
-- cfg['default-pagetype']
-- The pagetype to display for pages other than templates or Lua modules.
cfg['default-pagetype'] = 'page'
----------------------------------------------------------------------------------------------------
-- Doc link configuration
----------------------------------------------------------------------------------------------------
-- cfg['doc-subpage']
-- The name of the subpage typically used for documentation pages.
cfg['doc-subpage'] = 'doc'
-- cfg['docpage-preload']
-- Preload file for template documentation pages in all namespaces.
cfg['docpage-preload'] = 'Template:Documentation/preload'
-- cfg['module-preload']
-- Preload file for Lua module documentation pages.
cfg['module-preload'] = 'Template:Documentation/preload-module-doc'
----------------------------------------------------------------------------------------------------
-- HTML and CSS configuration
----------------------------------------------------------------------------------------------------
-- cfg['templatestyles']
-- The name of the TemplateStyles page where CSS is kept.
-- Sandbox CSS will be at Module:Documentation/sandbox/styles.css when needed.
cfg['templatestyles'] = 'Module:Documentation/styles.css'
-- cfg['container']
-- Class which can be used to set flex or grid CSS on the
-- two child divs documentation and documentation-metadata
cfg['container'] = 'documentation-container'
-- cfg['main-div-classes']
-- Classes added to the main HTML "div" tag.
cfg['main-div-classes'] = 'documentation'
-- cfg['main-div-heading-class']
-- Class for the main heading for templates and modules and assoc. talk spaces
cfg['main-div-heading-class'] = 'documentation-heading'
-- cfg['start-box-class']
-- Class for the start box
cfg['start-box-class'] = 'documentation-startbox'
-- cfg['start-box-link-classes']
-- Classes used for the [view][edit][history] or [create] links in the start box.
-- mw-editsection-like is per [[Wikipedia:Village pump (technical)/Archive 117]]
cfg['start-box-link-classes'] = 'mw-editsection-like plainlinks'
-- cfg['end-box-class']
-- Class for the end box.
cfg['end-box-class'] = 'documentation-metadata'
-- cfg['end-box-plainlinks']
-- Plainlinks
cfg['end-box-plainlinks'] = 'plainlinks'
-- cfg['toolbar-class']
-- Class added for toolbar links.
cfg['toolbar-class'] = 'documentation-toolbar'
-- cfg['clear']
-- Just used to clear things.
cfg['clear'] = 'documentation-clear'
----------------------------------------------------------------------------------------------------
-- Tracking category configuration
----------------------------------------------------------------------------------------------------
-- cfg['display-strange-usage-category']
-- Set to true to enable output of cfg['strange-usage-category'] if the module is used on a /doc subpage
-- or a /testcases subpage. This should be a boolean value (either true or false).
cfg['display-strange-usage-category'] = true
-- cfg['strange-usage-category']
-- Category to output if cfg['display-strange-usage-category'] is set to true and the module is used on a
-- /doc subpage or a /testcases subpage.
cfg['strange-usage-category'] = 'Wikipedia pages with strange ((documentation)) usage'
--[[
----------------------------------------------------------------------------------------------------
-- End configuration
--
-- Don't edit anything below this line.
----------------------------------------------------------------------------------------------------
--]]
return cfg
d70e8b1402a2bbe08a1fef4b75d743e661af0c95
Template:Header
10
23
40
39
2023-08-27T20:01:05Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
{| style="width: 100% !important;"
|-
| style="border-top: 4px solid #{{{topbarhex|6F6F6F}}}; background-color: #{{{bodyhex|F6F6F6}}}; padding: 10px 15px;" | {{#if:{{{shortcut|}}}| {{shortcut|{{{shortcut|uselang={{{uselang|{{CURRENTCONTENTLANGUAGE}}}}}}}}}}}}<div style="font-size:180%; text-align: left; color: {{{titlecolor|}}}">'''{{{title|{{{1|{{BASEPAGENAME}}}}}}}}'''</div>
<div style="padding-top:0.3em; padding-bottom:0.1em; font-size:100%; text-align: left; color: {{{bodycolor|}}}">{{{notes|Put some notes here!}}}</div>
|-
| style="height: 10px" |
|}
{{clear}}<noinclude>{{documentation}}[[Category:templates]]</noinclude>
03aac86137ab11bfccbcceb2de919475af2953dd
Template:Agree
10
24
42
41
2023-08-27T20:01:06Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Symbol confirmed.svg|18px|link=]] '''{{{1|Agree}}}'''<noinclude>{{documentation}}</noinclude>
775ddedaccda0d477a1b3c82d422e3760c862609
Template:Comment
10
25
44
43
2023-08-27T20:01:06Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Pictogram voting comment.svg|18px|link=]] '''{{{1|Comment:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude>
5f48a21f6ec3dc6d82cfa7a668c9e00fec175396
Template:Doing
10
26
46
45
2023-08-27T20:01:07Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Pictogram voting wait.svg|18px|link=|alt=]] '''{{{1|Doing…}}}'''<noinclude>
{{documentation}}
[[Category:Resolution templates]]
</noinclude>
eb1feacb3d7a14829c6a9be311732ee0d24d35f3
Template:Custom resolution
10
27
48
47
2023-08-27T20:01:08Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
<span class="nowrap">[[File:{{{1|Cancelled process mini.svg}}}|18px|alt={{{2|Text here}}}]] <span style="{{{3|">'''{{{2|Text here}}}'''</span></span>
<noinclude>{{Documentation|content=
This template allows for the creation of custom [[Template:Template list#Resolution templates|resolution templates]] using 2 parameters.
}}[[Category:Resolution templates]]</noinclude>
a563b1f700886c4f97480a7ee81988b33af01ccf
Template:Not done
10
28
50
49
2023-08-27T20:01:09Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:X mark.svg|18px]] '''{{{1|Not done}}}'''<noinclude>
{{documentation}}
[[Category:Resolution templates]]
</noinclude>
fd025e245bee74ddd6c5ae757f983a3cd3258d94
Template:Done
10
29
52
51
2023-08-27T20:01:09Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
<span class="nowrap">[[File:Yes check.svg|18px|link=|alt=]] '''{{{1|Done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>
{{documentation}}
[[Category:Resolution templates]]</noinclude>
717c1385d516cd84dc05a10ba88359a52c9d8415
Template:Note
10
30
54
53
2023-08-27T20:01:10Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Pictogram voting info.svg|18px|link=]] '''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
4d5cae62908f9cc8da2988712b236fe939bc80e2
Template:Endorse
10
31
56
55
2023-08-27T20:01:11Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Symbol support2 vote.svg|link=|alt=|16px|]] '''{{{1|Endorse}}}'''<noinclude>{{Documentation}}</noinclude>
23cc6c948818ca6949bd9af0991af58f1858483f
Template:High priority
10
32
58
57
2023-08-27T20:01:11Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Exclamationdiamond.svg|20px|link=]] '''{{{1|High Priority}}}'''{{{{{|safesubst:}}}#if:{{{note|{{{reason|}}}}}}|<nowiki />: {{{note|{{{reason}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
65d49ca7f928fef46651d89d894267497560a60b
Template:Idea
10
33
60
59
2023-08-27T20:01:12Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Dialog-information on.svg|18px|link=]] '''{{{1|Idea}}}:'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
e4062daed60634ce9e9cd2f052d9102bcf7e2916
Template:In progress
10
34
62
61
2023-08-27T20:01:13Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
[[File:Pictogram voting info.svg|16px|link=|alt=]] '''{{{1|In Progress}}}'''<noinclude>{{documentation}}</noinclude>
ff9e3ff4245b3dcc9ae45a1f8e3c7e7830fc0fff
Special:Badtitle/NS200:Alxira5
200
35
63
2023-08-27T20:03:41Z
Alxira5
4
import user wiki
wikitext
text/x-wiki
da39a3ee5e6b4b0d3255bfef95601890afd80709
Template:Clear
10
36
65
64
2023-08-28T02:15:01Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<div style="clear:{{{1|both}}};"></div><noinclude>
{{documentation}}
</noinclude>
38bab3e3d7fbd3d6800d46556e60bc6bac494d72
Template:On hold
10
37
67
66
2023-08-28T02:15:02Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Symbol wait.svg|18px]] '''{{{1|On hold}}}'''<noinclude>[[Category:Resolution templates]]<noinclude>
{{documentation}}
[[Category:Resolution templates]]</noinclude>
7a18e8aa8c80a33b1a68eed60d0993a75202162f
Template:Partly done
10
38
69
68
2023-08-28T02:15:02Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<span class="nowrap">[[File:Yellow_check.svg|18px|link=|alt=]] '''{{{1|Partly done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
24a90b5a5c4c716b7ec12889fbd09a1da2ba1ca3
Template:Pending
10
39
71
70
2023-08-28T02:15:03Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{On hold|{{{1|Pending}}}}}<noinclude>{{Documentation}}</noinclude>
3d534f8f2cf14f73be843d306efcecbff05c7f5e
Template:Question
10
40
73
72
2023-08-28T02:15:03Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Pictogram voting question.svg|18px|link=]] '''{{{1|Question:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude>
9fae3d5ccc70d95a5a7de8983d7a82c1a55853e3
Template:Resolved
10
41
75
74
2023-08-28T02:15:04Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<span class="nowrap">[[File:Yes check.svg|18px|link=]] '''{{{1|Resolved}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|: {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
bcebb832c81fc395e8891f82747510f76292cb34
Template:Reviewing
10
42
77
76
2023-08-28T02:15:04Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Pictogram voting wait green.svg|17px|link=]] '''{{{1|Reviewing}}}...'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
0184f75a66f991d9eb99f23a75df36dd184e0c4b
Template:Thank you
10
43
79
78
2023-08-28T02:15:05Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<span class="nowrap">[[File:Face-smile.svg|18px|link=]] '''{{{1|Thank you}}}'''</span><noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
4312420b6485d1eb316af5c56f663a7d618afb9b
Template:Withdrawn
10
44
81
80
2023-08-28T02:15:05Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Cancelled process mini.svg|200x20px|link=|alt=]] '''{{{1|Request withdrawn}}}'''<noinclude>{{documentation}}</noinclude>
24c0cd218d3a61ac8b524c6f8d1b5cc405ca3d80
Template:Working
10
45
83
82
2023-08-28T02:15:06Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Icon tools.svg|20px|link=]] '''{{{1|Working}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>
0619210f08d5114b9a348b4f1045a0b6f4552012
Template:Documentation/mirror
10
46
85
84
2023-08-28T02:15:06Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<includeonly>{{subst:msgnw:{{subst:NAMESPACE}}:{{subst:BASEPAGENAME}}}}</includeonly><noinclude>{{doc}}</noinclude>
2f3df1c981931719e821f054f3db0c67072f781e
Template:Documentation/preload
10
47
87
86
2023-08-28T02:15:06Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Documentation subpage}}
== Usage ==
<include<includeonly></includeonly>only>
<!-- Categories below this line -->
}}</include<includeonly></includeonly>only><noinclude>
{{Documentation|content=
This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template documentation page.
[[Category:Documentation preloads]]
}}</noinclude>
587413cafd960a3f7f3c9257a4160f3654fdbe0f
Template:Documentation/preload-sandbox
10
48
89
88
2023-08-28T02:15:07Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<!--
Add your experimental template code here.
--><noin<includeonly></includeonly>clude>
{{Documentation}}
</noin<includeonly></includeonly>clude>
c7b7f3f85c510513f7415b512c03742fe61ee31a
Template:Documentation/preload-testcases
10
49
91
90
2023-08-28T02:15:08Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude>
9e9c7d43373bd7aeaee3293842ae54db0257e549
Template:Delete
10
50
93
92
2023-08-28T02:15:08Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{MessageBox
|Flag color=firebrick
|Border color=firebrick
|Background color=#FFEEEE
|Image=[[File:Trash Can.svg|80px]]
|Message text=<span style="line-height:2;"><span style="color:red; line-height:1.2;">'''This article is a candidate for speedy deletion because {{{1}}}. '''</span><br><span style="color:#000000;">
Deleting Reason: {{{1|No reason given}}}</span></span>
}}<noinclude>[[Category:Notice templates]]</noinclude>
<includeonly>[[Category:Candidates for deletion]]</includeonly>
<noinclude>
<languages />
<translate>
<!--T:1-->
== Usage Note ==
Add this template to any page on this wiki for which you're requesting an [[<tvar name=admin>mw:Special:MyLanguage/Manual:Administrators</tvar>|administrator]] to delete, either by adding it to the very top of the page (preferred) or by replacing the existing content with this template (also acceptable) following the format prescribed below.</translate>
<code><nowiki>{{Delete|1=</nowiki>''Your deletion reason''<nowiki>}}</nowiki></code>
<translate>
<!--T:2-->
Replace ''your deletion reason'' with one of the commonly accepted reasons for deletion below, or describe concisely ''why'' you are requesting deletion.
<!--T:3-->
If you do not specify a reason in parameter <code>1=</code>, ''no deletion reason'' will be inserted, and your request ''may'' be declined if it is not apparent why deletion is being requested.
<!--T:4-->
=== Commonly accepted reasons for deletion ===
* Vandalism
* Attack page/page created solely for harassment
* Copyright violation
* Spam
* Test page. Please either use the [[<tvar name=sb>m:Meta:Sandbox</tvar>|community sandbox]] or [[<tvar name=mypsb>Special:MyPage/sandbox</tvar>|create your personal sandbox]]
* Non-controversial housekeeping
* [[<tvar name=br>Special:BrokenRedirects</tvar>|Broken redirect]]
* [[<tvar name=dr>Special:DoubleRedirects</tvar>|Double redirect]]
* Author requests deletion, or author blanked
* Subpages with no parent page
* Talk pages with no companion page and no meaningful discussion history
* Images available as identical copies on either [[<tvar name=commons>commons:Special:MyLanguage/Main Page</tvar>|Miraheze Commons]] or [[wikimediacommons:|Wikimedia Commons]]
* [[<tvar name=uc>Special:UnusedCategories</tvar>|Empty category]]
* [[<tvar name=ut>Special:UnusedTemplates</tvar>|Unused template (including a template redirect)]] with no inlinks, transclusions, or page watchers
=== Parameter(s) === <!--T:5-->
</translate>
<templatedata>
{
"params": {
"1": {
"label": "Deletion reason",
"example": "Author requests deletion, or author blanked",
"default": "No deletion reason",
"suggested": true,
"description": "Template to add to any page requiring deletion."
}
}
}
</templatedata>
</noinclude>
ae465d4609d3bbb646339e90ae469c31ce34a9df
Template:Userbox
10
51
95
94
2023-08-28T02:15:09Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{#invoke:userbox|userbox}}<noinclude>{{documentation}}</noinclude>
6813e8e31cadc62df2379b5fae9ea23c23f29e97
Template:Information
10
52
97
96
2023-08-28T02:15:09Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<templatestyles src="Information/style.css" />
<div class="hproduct commons-file-information-table">
<table class="toccolours vevent fileinfotpl-type-information" style="width: 100%;" cellpadding="4">
<!-- Description -->
<tr style="vertical-align: top">
<td id="fileinfotpl_desc" class="fileinfo-paramfield">Description<span class="summary fn" style="display:none">{{PAGENAME}}</span></td>
<td class="description">{{ #if: {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{Description missing}} }}</td>
</tr>
<!-- Source -->
<tr style="vertical-align: top">
<td id="fileinfotpl_src" class="fileinfo-paramfield">Source</td>
<td>{{ #if: {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{Description missing|source information}} }}</td>
</tr>
<!-- Author -->
<tr style="vertical-align: top">
<td id="fileinfotpl_aut" class="fileinfo-paramfield">Author</td>
<td>{{ #if: {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{Description missing|author information}} }}</td>
</tr>
<!-- Fecha -->
<tr style="vertical-align: top">
<td id="fileinfotpl_aut" class="fileinfo-paramfield">Date</td>
<td>{{{date|{{{Date|{{{fecha|{{{Fecha|}}}}}}}}}}}}</td>
</tr>
<!-- Other versions -->
{{#switch: {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}{{{demo|<noinclude>1</noinclude>}}}
| =
| - =
| none =
| #default =
<tr style="vertical-align: top">
<td id="fileinfotpl_ver" class="fileinfo-paramfield" style="background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight:bold">Other versions</td>
<td>
{{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}
</td>
</tr>
}}
</table>
</div><noinclude>{{Documentation}}</noinclude>
3a749131ebeff40d99673728a045f76f81456cc1
Template:Information/style.css
10
53
99
98
2023-08-28T02:15:10Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
text
text/plain
.fileinfo-paramfield {
background: #ccf;
text-align: right;
padding-right: 0.4em;
width: 15%;
font-weight: bold;
}
/* [[Category:Template stylesheets]] */
396fcf8276bedcc9dad608bdbd9bf1be7f90424d
Template:Talk quote inline
10
54
101
100
2023-08-28T02:15:10Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<templatestyles src="Talk quote inline/styles.css" /><!--
--><q {{#if: {{{title|}}} | title="{{{title}}}"}} class="inline-quote-talk {{#if: {{{i|{{{italic|}}}}}} | inline-quote-talk-italic}} {{#if: {{{q|{{{quotes|}}}}}}|inline-quote-talk-marks}}">{{{1|Example text}}}</q><!--
--><noinclude>
{{Documentation}}
</noinclude>
b18e2fdc57277adbf4e3f4f513e78ecc5831453f
Template:Soft redirect
10
55
103
102
2023-08-28T02:15:11Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
__NONEWSECTIONLINK__[[File:Softredirarrow.svg|64px|Soft redirect to:|link=]]<span class="redirectText" id="softredirect">[[:{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}|{{{2|{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}}}}]]</span><br /><span style="font-size:85%; padding-left:48px;">This page is a [[metawikimedia:soft redirect|soft redirect]].</span><noinclude>
{{Documentation}}
</noinclude>
a965c0fe43aa0fe8f0e17ed40d725f0e7b3649f6
Template:See also
10
56
105
104
2023-08-28T02:15:11Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{hatnote|extraclasses=boilerplate seealso|{{{altphrase|See also}}}: {{#if:{{{1<includeonly>|</includeonly>}}} |<!--then:-->[[:{{{1}}}{{#if:{{{label 1|{{{l1|}}}}}}|{{!}}{{{label 1|{{{l1}}}}}}}}]] |<!--else:-->'''Error: [[Template:See also|Template must be given at least one article name]]'''
}}{{#if:{{{2|}}}|{{#if:{{{3|}}}|, | and }} [[:{{{2}}}{{#if:{{{label 2|{{{l2|}}}}}}|{{!}}{{{label 2|{{{l2}}}}}}}}]]
}}{{#if:{{{3|}}}|{{#if:{{{4|}}}|, |, and }} [[:{{{3}}}{{#if:{{{label 3|{{{l3|}}}}}}|{{!}}{{{label 3|{{{l3}}}}}}}}]]
}}{{#if:{{{4|}}}|{{#if:{{{5|}}}|, |, and }} [[:{{{4}}}{{#if:{{{label 4|{{{l4|}}}}}}|{{!}}{{{label 4|{{{l4}}}}}}}}]]
}}{{#if:{{{5|}}}|{{#if:{{{6|}}}|, |, and }} [[:{{{5}}}{{#if:{{{label 5|{{{l5|}}}}}}|{{!}}{{{label 5|{{{l5}}}}}}}}]]
}}{{#if:{{{6|}}}|{{#if:{{{7|}}}|, |, and }} [[:{{{6}}}{{#if:{{{label 6|{{{l6|}}}}}}|{{!}}{{{label 6|{{{l6}}}}}}}}]]
}}{{#if:{{{7|}}}|{{#if:{{{8|}}}|, |, and }} [[:{{{7}}}{{#if:{{{label 7|{{{l7|}}}}}}|{{!}}{{{label 7|{{{l7}}}}}}}}]]
}}{{#if:{{{8|}}}|{{#if:{{{9|}}}|, |, and }} [[:{{{8}}}{{#if:{{{label 8|{{{l8|}}}}}}|{{!}}{{{label 8|{{{l8}}}}}}}}]]
}}{{#if:{{{9|}}}|{{#if:{{{10|}}}|, |, and }} [[:{{{9}}}{{#if:{{{label 9|{{{l9|}}}}}}|{{!}}{{{label 9|{{{l9}}}}}}}}]]
}}{{#if:{{{10|}}}|{{#if:{{{11|}}}|, |, and }} [[:{{{10}}}{{#if:{{{label 10|{{{l10|}}}}}}|{{!}}{{{label 10|{{{l10}}}}}}}}]]
}}{{#if:{{{11|}}}|{{#if:{{{12|}}}|, |, and }} [[:{{{11}}}{{#if:{{{label 11|{{{l11|}}}}}}|{{!}}{{{label 11|{{{l11}}}}}}}}]]
}}{{#if:{{{12|}}}|{{#if:{{{13|}}}|, |, and }} [[:{{{12}}}{{#if:{{{label 12|{{{l12|}}}}}}|{{!}}{{{label 12|{{{l12}}}}}}}}]]
}}{{#if:{{{13|}}}|{{#if:{{{14|}}}|, |, and }} [[:{{{13}}}{{#if:{{{label 13|{{{l13|}}}}}}|{{!}}{{{label 13|{{{l13}}}}}}}}]]
}}{{#if:{{{14|}}}|{{#if:{{{15|}}}|, |, and }} [[:{{{14}}}{{#if:{{{label 14|{{{l14|}}}}}}|{{!}}{{{label 14|{{{l14}}}}}}}}]]
}}{{#if:{{{15|}}}|, and [[:{{{15}}}{{#if:{{{label 15|{{{l15|}}} }}}|{{!}}{{{label 15|{{{l15|}}} }}} }}]]
}}{{#if:{{{16|}}}| — '''<br/>Error: [[Template:See also|Too many links specified (maximum is 15)]]'''
}}}}<noinclude>
{{documentation}}
</noinclude>
0315f43d7e4b679054955c7a50fe554ab1df63de
Template:=
10
57
107
106
2023-08-28T02:15:12Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
=<noinclude>
{{documentation}}
</noinclude>
44f3105df6073eb65369938814d1551b51611402
Template:Para
10
58
109
108
2023-08-28T02:15:13Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<code class="tpl-para" style="word-break:break-word;{{SAFESUBST:<noinclude />#if:{{{plain|}}}|border: none; background-color: inherit;}} {{SAFESUBST:<noinclude />#if:{{{style|}}}|{{{style}}}}}">|{{SAFESUBST:<noinclude />#if:{{{1|}}}|{{{1}}}=}}{{{2|}}}</code><noinclude>
{{Documentation}}
<!--Categories and interwikis go near the bottom of the /doc subpage.-->
</noinclude>
7be5bee75307eae9342bbb9ff3a613e93e93d5a7
Template:Ping
10
59
111
110
2023-08-28T02:15:13Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{{{{|safesubst:}}}#invoke:Reply to|replyto|<noinclude>example=Example</noinclude>|max=50}}<noinclude>{{documentation}}</noinclude>
0a7b3547181e17a03ec99855e276688fcc36ce1e
Template:Abstain
10
60
113
112
2023-08-28T02:15:14Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Symbol neutral vote.svg|18px]] '''<bdi>{{{1|Abstain}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
b1098b70832376165658562bfc8de5b6187bdb26
Template:Neutral
10
61
115
114
2023-08-28T02:15:15Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
[[File:Symbol neutral vote.svg|18px]] '''<bdi>{{{1|Neutral}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
59552c46cb01ccf2c6196bdea9ec3eb90858e675
Template:Oppose
10
62
117
116
2023-08-28T02:15:15Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{ #switch: {{{4|{{{1|}}}}}}
| Regular= [[File:Symbol oppose vote.png|18px|alt=]]
| Normal= [[File:Symbol oppose vote.png|18px|alt=]]
| Strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| Strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| Strongest = [[File:Symbol full oppose vote.svg|20px|alt=]]
| Weak= [[File:Weak Oppose.png|18px|alt=]]
| Weakly= [[File:Weak Oppose.png|18px|alt=]]
| strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]]
| weak= [[File:Weak Oppose.png|18px|alt=]]
| weakly= [[File:Weak Oppose.png|18px|alt=]]
| strongest = [[File:Symbol full oppose vote.svg|20px|alt=]]
|#default= [[File:Symbol oppose vote.svg|18px|alt=]]
}} {{ #switch: {{{1|}}}
| Regular='''Oppose'''
| Normal= '''Oppose'''
| Strongest = '''Strongest oppose'''
| Strongly= '''Strongly oppose'''
| Strong= '''Strong oppose'''
| Weak= '''Weak oppose'''
| Weakly= '''Weakly oppose'''
| strongly= '''Strongly oppose'''
| strong= '''Strong oppose'''
| weak= '''Weak oppose'''
| weakly= '''Weakly oppose'''
| strongest = '''Strongest oppose'''
| {{{other|2}}} = '''{{{3}}}'''
|#default= '''Oppose'''
}}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
8c57115a1c36446a717d2c874b2895b057d1ffc3
Template:Support
10
63
119
118
2023-08-28T02:15:16Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{ #switch: {{{4|{{{1|}}}}}}
| Regular= [[File:Symbol support vote.svg|18px|alt=]]
| Normal= [[File:Symbol support vote.svg|18px|alt=]]
| Strongly= [[File:Symbol strong support vote.svg|18px|alt=]]
| Strongest= [[File:Symbol full support vote.svg|22px]]
| Strong= [[File:Symbol strong support vote.svg|18px|alt=]]
| Weak= [[File:Symbol partial support vote.svg|18px|alt=]]
| Weakly= [[File:Symbol partial support vote.svg|18px|alt=]]
| strongly= [[File:Symbol strong support vote.svg|18px|alt=]]
| strong= [[File:Symbol strong support vote.svg|18px|alt=]]
| weak= [[File:Symbol partial support vote.svg|18px|alt=]]
| weakly= [[File:Symbol partial support vote.svg|18px|alt=]]
|#default= [[File:Symbol support vote.svg|18px|alt=]]
}} {{ #switch: {{{1|}}}
| Regular='''Support'''
| Normal= '''Support'''
| Strongly= '''Strongly support'''
| Strong= '''Strong support'''
| Weak= '''Weak support'''
| Weakly= '''Weakly support'''
| strongly= '''Strongly support'''
| Strongest= '''''Strongest support'''''
| strong= '''Strong support'''
| weak= '''Weak support'''
| weakly= '''Weakly support'''
| {{{other|2}}} = '''{{{3}}}'''
|#default= '''Support'''
}}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude>
0a43c05b804693f20b74446f7e7e6d7ccd10c516
Template:User github
10
64
121
120
2023-08-28T02:15:16Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = [[File:GitLogo.png|43px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an account on GitHub{{#if:{{{account|}}}| as ''[[github:{{{account}}}|{{{account}}}]]''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use GitHub
}}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
08ef531d5b5a1e69b84939e0fc1f1d0d622f38ad
Template:User IRC
10
65
123
122
2023-08-28T02:15:17Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = #
| id-s = 24
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:IRC|IRC]]{{#if:{{{nick|}}}| as ''{{{nick}}}''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use IRC
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
a148152ff16bb6fc7f7a7bd46c4898b50f1996fc
Template:User discord
10
66
125
124
2023-08-28T02:15:17Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = #
| id-s = 24
| id-fc = #5865F2
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:Discord|Discord]]{{#if:{{{account|}}}| as ''{{{account}}}''|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use Discord
}}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
19b1d90000718152b9058c16c7c1ba13d7cb2715
Template:User wikimedia
10
67
127
126
2023-08-28T02:15:18Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = [[File:Wikimedia Foundation Logo.png|43px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an [[metawikimedia:Special:CentralAuth/{{{account|{{BASEPAGENAME}}}}}|account]] at the Wikimedia Foundation projects.
| nocat = {{{nocat|}}}
| usercategory = Wikimedians
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
c7b06f2b4d088ee94d893eb1e3548e9e0562fc5e
Template:User youtube
10
68
129
128
2023-08-28T02:15:18Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = [[File:YouTube full-color icon (2017).svg|37px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has a YouTube channel{{#if:{{{account|}}}| at [https://{{{account}}} '''{{{account}}}''']|}}.
| nocat = {{{nocat|}}}
| usercategory = Users who use YouTube
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
f0ba1080f2a2d69494317a9790fa3d7e6e4239b4
Template:User instagram
10
69
131
130
2023-08-28T02:15:18Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{Userbox
| id = [[File:Instagram icon.png|37px]]
| float = {{{float|right}}}
| border-c = #808080
| id-c = #FFFFFF
| info-c = #DBDBDB
| info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an {{#if:{{{account|}}}| account [https://instagram.com/{{{account}}} '''@{{{account}}}''']|account}} on Instagram.
| nocat = {{{nocat|}}}
| usercategory = Users who use Instagram
}}<noinclude>{{Documentation}}
[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude>
1178ad0721de804c08dd554ebb4b52c4c6569fde
Template:Documentation subpage
10
70
133
132
2023-08-28T02:15:19Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<includeonly><!--
-->{{#ifeq:{{lc:{{SUBPAGENAME}}}} |{{{override|doc}}}
| <!--(this template has been transcluded on a /doc or /{{{override}}} page)-->
</includeonly><!--
-->{{#ifeq:{{{doc-notice|show}}} |show
| {{Mbox
| type = notice
| style = margin-bottom:1.0em;
| image = [[File:Edit-copy green.svg|40px|alt=|link=]]
| text =
'''This is a documentation subpage''' for '''{{{1|[[:{{SUBJECTSPACE}}:{{BASEPAGENAME}}]]}}}'''.<br/> It contains usage information, [[mw:Help:Categories|categories]] and other content that is not part of the original {{#if:{{{text2|}}} |{{{text2}}} |{{#if:{{{text1|}}} |{{{text1}}} | page}}}}.
}}
}}<!--
-->{{DEFAULTSORT:{{{defaultsort|{{PAGENAME}}}}}}}<!--
-->{{#if:{{{inhibit|}}} |<!--(don't categorize)-->
| <includeonly><!--
-->{{#ifexist:{{NAMESPACE}}:{{BASEPAGENAME}}
| [[Category:{{#switch:{{SUBJECTSPACE}} |Template=Template |Module=Module |User=User |#default=Wikipedia}} documentation pages]]
| [[Category:Documentation subpages without corresponding pages]]
}}<!--
--></includeonly>
}}<!--
(completing initial #ifeq: at start of template:)
--><includeonly>
| <!--(this template has not been transcluded on a /doc or /{{{override}}} page)-->
}}<!--
--></includeonly><noinclude>{{Documentation}}</noinclude>
471e685c1c643a5c6272e20e49824fffebad0448
Template:Mbox
10
71
135
134
2023-08-28T02:15:19Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{#invoke:Message box|mbox}}<noinclude>
{{documentation}}
<!-- Categories go on the /doc subpage, and interwikis go on Wikidata. -->
</noinclude>
c262e205f85f774a23f74119179ceea11751d68e
Template:MessageBox
10
72
137
136
2023-08-28T02:15:20Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<div style="width: {{#if:{{{width|}}}|{{{width}}}|80%}}; background-color: {{#if:{{{Background color}}}|{{{Background color}}}|#f5f5f5}}; border-top: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-bottom: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-right: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-left: 12px solid {{#if:{{{Flag color}}}|{{{Flag color}}}|#aaaaaa}}; margin: 0.5em auto 0.5em;">
{|
{{#if:{{{Image}}}|{{!}}style="width:93px; text-align:center; vertical-align:middle; padding-top:1px;padding-bottom:7px" {{!}} {{{Image}}} }}
|style="vertical-align:middle;padding-left:3px;padding-top:10px;padding-bottom:10px;padding-right:10px; background-color: {{#if:{{{Background color}}}{{!}}{{{Background color}}}{{!}}#f5f5f5}};" | {{{Message text}}}
|}
</div><noinclude>[[Category:Notice templates]]</noinclude>
c6727bf6179a36a5413ed93f232fd0e2f7180256
Template:Shortcut
10
73
139
138
2023-08-28T02:15:20Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<!--
Putting anchors on page:
--><div style="position: relative; top: -3em;">{{#if:{{{1|}}}|<span id="{{anchorencode:{{{1|}}}}}"></span> }}{{#if:{{{2|}}}|<span id="{{anchorencode:{{{2|}}}}}"></span> }}{{#if:{{{3|}}}|<span id="{{anchorencode:{{{3|}}}}}"></span> }}{{#if:{{{4|}}}|<span id="{{anchorencode:{{{4|}}}}}"></span> }}{{#if:{{{5|}}}|<span id="{{anchorencode:{{{5|}}}}}"></span> }}</div>
<table class="shortcutbox noprint" style="float: right; border: 1px solid #aaa; background: #fff; margin: .3em .3em .3em 1em; padding: 3px; text-align: center;"><tr><th style="border: none; background: transparent;" class="plainlist"><!--
Adding the shortcut links:
--><small>[[w:Wikipedia:Shortcut|Shortcut{{#if:{{{2|}}}|s}}]]:
{{#if:{{{1|}}}|<ul><li> [[{{{1}}}]]</li>
}}{{#if:{{{2|}}}|<li> [[{{{2}}}]]</li>
}}{{#if:{{{3|}}}|<li> [[{{{3}}}]]</li>
}}{{#if:{{{4|}}}|<li> [[{{{4}}}]]</li>
}}{{#if:{{{5|}}}|<li> [[{{{5}}}]]</li>
}}{{#if:{{{msg|}}}|<li> {{{msg}}}</li>
}}</ul></small></th></tr></table><noinclude>{{doc}}</noinclude>
96a4c79718f5cbdf3e074cc545193ea4e863d1fb
Template:Template link
10
74
141
140
2023-08-28T02:15:21Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{[[Template:{{{1}}}|{{{1}}}]]}}<noinclude>{{documentation}}
<!-- Categories go on the /doc subpage and interwikis go on Wikidata. -->
</noinclude>
eabbec62efe3044a98ebb3ce9e7d4d43c222351d
Template:Template link expanded
10
75
143
142
2023-08-28T02:15:21Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<code><nowiki>{{</nowiki>{{#if:{{{subst|}}} |[[Help:Substitution|subst]]:}}<!--
-->[[{{{sister|{{{SISTER|}}}}}}{{ns:Template}}:{{{1|}}}|{{{1|}}}]]<!--
-->{{#if:{{{2|}}} ||{{{2}}}}}<!--
-->{{#if:{{{3|}}} ||{{{3}}}}}<!--
-->{{#if:{{{4|}}} ||{{{4}}}}}<!--
-->{{#if:{{{5|}}} ||{{{5}}}}}<!--
-->{{#if:{{{6|}}} ||{{{6}}}}}<!--
-->{{#if:{{{7|}}} ||{{{7}}}}}<!--
-->{{#if:{{{8|}}} ||{{{8}}}}}<!--
-->{{#if:{{{9|}}} ||{{{9}}}}}<!--
-->{{#if:{{{10|}}} ||{{{10}}}}}<!--
-->{{#if:{{{11|}}} ||{{{11}}}}}<!--
-->{{#if:{{{12|}}} ||{{{12}}}}}<!--
-->{{#if:{{{13|}}} ||{{{13}}}}}<!--
-->{{#if:{{{14|}}} ||{{{14}}}}}<!--
-->{{#if:{{{15|}}} ||{{{15}}}}}<!--
-->{{#if:{{{16|}}} ||{{{16}}}}}<!--
-->{{#if:{{{17|}}} ||{{{17}}}}}<!--
-->{{#if:{{{18|}}} ||{{{18}}}}}<!--
-->{{#if:{{{19|}}} ||{{{19}}}}}<!--
-->{{#if:{{{20|}}} ||{{{20}}}}}<!--
-->{{#if:{{{21|}}} ||''...''}}<!--
--><nowiki>}}</nowiki></code><noinclude>
{{Documentation}}
</noinclude>
9f670205d4b358df089b1a820f78f02a88afca3a
Template:Discussion top
10
76
145
144
2023-08-28T02:15:21Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<div class="boilerplate metadata discussion-archived" style="background-color: #F2F4FC; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #aaa">
:The following discussion is closed. Please do not modify it. Subsequent comments should be made in a new section.
::{{{1|}}}
----<noinclude></div>{{documentation}}</noinclude>
c8b38525e188dbfa68b0e9cdd1864ceff2ed100e
Template:Discussion bottom
10
77
147
146
2023-08-28T02:15:22Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<noinclude><div></noinclude>----
:The above discussion is preserved as an archive. Please do not modify it. Subsequent comments should be made in a new section </div><noinclude>{{documentation|Template:Discussion top/doc}}</noinclude>
80d5baa979985b3b685585611b0e954d2c1c6e10
Template:Current time
10
78
149
148
2023-08-28T02:15:22Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{#switch:{{{1}}}
|Coordinated Universal Time=Current UTC is {{CURRENTTIME}}
|UTC-1=Current time for {{{1}}} is {{utc|23}}
|UTC-2=Current time for {{{1}}} is {{utc|22}}
|UTC-2:30=Current time for {{{1}}} is {{utc|21|30}}
|UTC-3=Current time for {{{1}}} is {{utc|21}}
|UTC-3:30=Current time for {{{1}}} is {{utc|20|30}}
|UTC-4=Current time for {{{1}}} is {{utc|20}}
|UTC-5=Current time for {{{1}}} is {{utc|19}}
|UTC-6=Current time for {{{1}}} is {{utc|18}}
|UTC-7=Current time for {{{1}}} is {{utc|17}}
|UTC-8=Current time for {{{1}}} is {{utc|16}}
|UTC-9=Current time for {{{1}}} is {{utc|15}}
|UTC-9:30=Current time for {{{1}}} is {{utc|14|30}}
|UTC-10=Current time for {{{1}}} is {{utc|14}}
|UTC-11=Current time for {{{1}}} is {{utc|13}}
|UTC-12=Current time for {{{1}}} is {{utc|12}}
|UTC+0:20=Current time for {{{1}}} is {{utc|0|20}}
|UTC+0:30=Current time for {{{1}}} is {{utc|0|30}}
|UTC+1=Current time for {{{1}}} is {{utc|1}}
|UTC+2=Current time for {{{1}}} is {{utc|2}}
|UTC+3=Current time for {{{1}}} is {{utc|3}}
|UTC+3:30=Current time for {{{1}}} is {{utc|3|30}}
|UTC+4=Current time for {{{1}}} is {{utc|4}}
|UTC+4:30=Current time for {{{1}}} is {{utc|4|30}}
|UTC+4:51=Current time for {{{1}}} is {{utc|4|51}}
|UTC+5=Current time for {{{1}}} is {{utc|5}}
|UTC+5:30=Current time for {{{1}}} is {{utc|5|30}}
|UTC+5:40=Current time for {{{1}}} is {{utc|5|40}}
|UTC+5:45=Current time for {{{1}}} is {{utc|5|45}}
|UTC+6=Current time for {{{1}}} is {{utc|6}}
|UTC+6:30=Current time for {{{1}}} is {{utc|6|30}}
|UTC+7=Current time for {{{1}}} is {{utc|7}}
|UTC+7:20=Current time for {{{1}}} is {{utc|7|20}}
|UTC+7:30=Current time for {{{1}}} is {{utc|7|30}}
|UTC+8=Current time for {{{1}}} is {{utc|8}}
|UTC+8:30=Current time for {{{1}}} is {{utc|8|30}}
|UTC+8:45=Current time for {{{1}}} is {{utc|8|45}}
|UTC+9=Current time for {{{1}}} is {{utc|9}}
|UTC+9:30=Current time for {{{1}}} is {{utc|9|30}}
|UTC+10=Current time for {{{1}}} is {{utc|10}}
|UTC+10:30=Current time for {{{1}}} is {{utc|10|30}}
|UTC+11=Current time for {{{1}}} is {{utc|11}}
|UTC+11:30=Current time for {{{1}}} is {{utc|11|30}}
|UTC+12=Current time for {{{1}}} is {{utc|12}}
|UTC+12:45=Current time for {{{1}}} is {{utc|12|45}}
|UTC+13=Current time for {{{1}}} is {{utc|13}}
|UTC+13:45=Current time for {{{1}}} is {{utc|13|45}}
|UTC+14=Current time for {{{1}}} is {{utc|14}}
|#default=Current time is {{CURRENTTIME}}
}}<noinclude>{{documentation|content=Returns the current time in a given timezone (defaulting to the timezone specified in [[Special:ManageWiki/settings#mw-section-localisation]], which in turn defaults to UTC)
== Examples ==
{{tlx|current time}} -> {{current time}}
{{tlx|current time|UTC+1}} -> {{current time|UTC+1}}
{{tlx|current time|UTC-5}} -> {{current time|UTC-5}}
[[Category:Templates]]
}}</noinclude>
84d7f12dbea154240f9fa86372863cd6152dd98b
Template:Utc
10
79
151
150
2023-08-28T02:15:22Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
{{#time:H:i|{{#expr:{{{1|0}}} * 60 + {{{2|0}}} round 0}} minutes}}<noinclude>
{{documentation}}
<!-- PLEASE ADD CATEGORIES AND INTERWIKIS TO THE /doc SUBPAGE, THANKS -->
</noinclude>
d24309676bfe4692d038657a7171952e7d1cded7
Template:Hatnote
10
80
153
152
2023-08-28T02:15:23Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<div style="margin-left:2em; margin-right: 2em;>''{{{1}}}''</div>
<!-- The wikipedia templates uses :, which generated dd and dt tags. That is not ideal for accessibility. --><noinclude>
This is a general purpose template for all kind of [https://en.wikipedia.org/wiki/Wikipedia:Hatnote hatnotes]. '''Hatnotes''' are a small annotation above a page or a section that can help the reader navigate. It's used for example to clarify what a section is about and link to other pages the reader may want to read.
== Example usage ==
<pre><nowiki>
=== Heading ===
{{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}}
The first sentence of the section is here.
</nowiki></pre>
=== Heading ===
{{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}}
The first sentence of the section is here.
<templatedata>
{
"params": {
"1": {
"label": "content",
"description": "the content of the note",
"example": "For the xxx see [[yyy]]."
}
},
"description": "Adds a annotation for the reader about the content that follows. Usually used after a heading."
}
</templatedata>
[[Category:Templates]]
</noinclude>
5e58f83d6fa53ed06f85139184aff1d651804efe
Template:Description missing
10
81
155
154
2023-08-28T02:15:23Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<div class="boilerplate metadata" id="cleanup" style="text-align: center; background: #ffe; margin: .75em 15%; padding: .5em; border: 1px solid #e3e3b0;">
This media has no '''{{ #if: {{{1|}}} | {{{1}}} | description }}''', and may be lacking other information.
<br>
Media should have a summary to inform others of the content, author, source, and date if possible. If you know or have access to such information, please add it to the image page.
</div>
<includeonly>{{#switch:{{NAMESPACE}}|{{ns:6}}=|#default={{#ifeq:{{{category|}}}|no||[[Category:Images lacking a description|{{PAGENAME}}]]}}}}</includeonly><noinclude>
{{documentation}}
</noinclude>
2b5026cefd37c307f7f2ee331289c38741f834a5
MediaWiki:Gadget-morebits.js
8
82
157
156
2023-08-28T02:15:24Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
javascript
text/javascript
// <nowiki>
/**
* morebits.js
* ===========
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - Morebits.quickForm class - generates quick HTML forms on the fly
* - Morebits.wiki.api class - makes calls to the MediaWiki API
* - Morebits.wiki.page class - modifies pages on the wiki (edit, revert, delete, etc.)
* - Morebits.wikitext class - contains some utilities for dealing with wikitext
* - Morebits.status class - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - Morebits.simpleWindow class - a wrapper for jQuery UI Dialog with a custom look and extra features
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - Morebits.quickForm, Morebits.simpleWindow, and Morebits.status rely on the "morebits.css" file for their styling.
* - Morebits.simpleWindow relies on jquery UI Dialog (ResourceLoader module name 'jquery.ui').
* - Morebits.quickForm tooltips rely on Tipsy (ResourceLoader module name 'jquery.tipsy').
* For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy].
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* * GadgetName[ResourceLoader|dependencies=mediawiki.util,jquery.ui,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js
*
* Most of the stuff here doesn't work on IE < 9. It is your script's responsibility to enforce this.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[Wikipedia talk:Twinkle]] on English Wikipedia [http://en.wikipedia.org].
* The latest development source is available at [https://github.com/azatoth/twinkle/blob/master/morebits.js].
*
* From simplewiki
*/
( function ( window, document, $, undefined ) { // Wrap entire file with anonymous function
var Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* **************** Morebits.userIsInGroup() ****************
* Simple helper function to see what groups a user might belong
*/
Morebits.userIsInGroup = function ( group ) {
return $.inArray(group, mw.config.get( 'wgUserGroups' )) !== -1;
}
/**
* **************** Morebits.isIPAddress() ****************
* Helper function: Returns true if given string contains a valid IPv4 or
* IPv6 address
*/
Morebits.isIPAddress = function ( address ) {
return mw.util.isIPv4Address(address) || mw.util.isIPv6Address(address);
};
/**
* **************** Morebits.sanitizeIPv6() ****************
* JavaScript translation of the MediaWiki core function IP::sanitizeIP() in
* includes/utils/IP.php.
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
*/
Morebits.sanitizeIPv6 = function ( address ) {
address = address.trim();
if ( address === '' ) {
return null;
}
if ( mw.util.isIPv4Address( address ) || !mw.util.isIPv6Address( address ) ) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
var abbrevPos = address.indexOf( '::' );
if ( abbrevPos > -1 ) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
var CIDRStart = address.indexOf( '/' );
var addressEnd = ( CIDRStart > -1 ) ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
var repeat, extra, pad;
if ( abbrevPos === 0 ) {
repeat = '0:';
extra = ( address == '::' ) ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if ( abbrevPos === ( addressEnd - 1 ) ) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
var replacement = repeat;
pad -= address.split( ':' ).length - 1;
for ( var i = 1; i < pad; i++ ) {
replacement += repeat;
}
replacement += extra;
address = address.replace( '::', replacement );
}
// Remove leading zeros from each bloc as needed
address = address.replace( /(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2' );
return address;
};
/**
* **************** Morebits.quickForm ****************
* Morebits.quickForm is a class for creation of simple and standard forms without much
* specific coding.
*
* Index to Morebits.quickForm element types:
*
* select A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event
* option An element for a combo box.
* - Attributes: value, label, selected, disabled
* optgroup A group of "option"s.
* - Attributes: label, list
* field A fieldset (aka group box).
* - Attributes: name, label
* checkbox A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* radio A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* input A text box.
* - Attributes: name, label, value, size, disabled, readonly, maxlength, event
* dyninput A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, sublabel, value, size, maxlength, event
* hidden An invisible form field.
* - Attributes: name, value
* header A level 5 header.
* - Attributes: label
* div A generic placeholder element or label.
* - Attributes: name, label
* submit A submit button. Morebits.simpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* button A generic button.
* - Attributes: name, label, disabled, event
* textarea A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, readonly
*
* Global attributes: id, style, tooltip, extra, adminonly
*/
Morebits.quickForm = function QuickForm( event, eventType ) {
this.root = new Morebits.quickForm.element( { type: 'form', event: event, eventType:eventType } );
};
Morebits.quickForm.prototype.render = function QuickFormRender() {
var ret = this.root.render();
ret.names = {};
return ret;
};
Morebits.quickForm.prototype.append = function QuickFormAppend( data ) {
return this.root.append( data );
};
Morebits.quickForm.element = function QuickFormElement( data ) {
this.data = data;
this.childs = [];
this.id = Morebits.quickForm.element.id++;
};
Morebits.quickForm.element.id = 0;
Morebits.quickForm.element.prototype.append = function QuickFormElementAppend( data ) {
var child;
if( data instanceof Morebits.quickForm.element ) {
child = data;
} else {
child = new Morebits.quickForm.element( data );
}
this.childs.push( child );
return child;
};
// This should be called without parameters: form.render()
Morebits.quickForm.element.prototype.render = function QuickFormElementRender( internal_subgroup_id ) {
var currentNode = this.compute( this.data, internal_subgroup_id );
for( var i = 0; i < this.childs.length; ++i ) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild( this.childs[i].render() );
}
return currentNode[0];
};
Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute( data, in_id ) {
var node;
var childContainder = null;
var label;
var id = ( in_id ? in_id + '_' : '' ) + 'node_' + this.id;
if( data.adminonly && !Morebits.userIsInGroup( 'sysop' ) ) {
// hell hack alpha
data.type = 'hidden';
}
var i, current, subnode;
switch( data.type ) {
case 'form':
node = document.createElement( 'form' );
node.className = "quickform";
node.setAttribute( 'action', 'javascript:void(0);');
if( data.event ) {
node.addEventListener( data.eventType || 'submit', data.event , false );
}
break;
case 'select':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.setAttribute( 'for', id );
label.appendChild( document.createTextNode( data.label ) );
}
var select = node.appendChild( document.createElement( 'select' ) );
if( data.event ) {
select.addEventListener( 'change', data.event, false );
}
if( data.multiple ) {
select.setAttribute( 'multiple', 'multiple' );
}
if( data.size ) {
select.setAttribute( 'size', data.size );
}
select.setAttribute( 'name', data.name );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
current = data.list[i];
if( current.list ) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute( current );
select.appendChild( subnode[0] );
}
}
childContainder = select;
break;
case 'option':
node = document.createElement( 'option' );
node.values = data.value;
node.setAttribute( 'value', data.value );
if( data.selected ) {
node.setAttribute( 'selected', 'selected' );
}
if( data.disabled ) {
node.setAttribute( 'disabled', 'disabled' );
}
node.setAttribute( 'label', data.label );
node.appendChild( document.createTextNode( data.label ) );
break;
case 'optgroup':
node = document.createElement( 'optgroup' );
node.setAttribute( 'label', data.label );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
current = data.list[i];
current.type = 'option'; //must be options here
subnode = this.compute( current );
node.appendChild( subnode[0] );
}
}
break;
case 'field':
node = document.createElement( 'fieldset' );
label = node.appendChild( document.createElement( 'legend' ) );
label.appendChild( document.createTextNode( data.label ) );
if( data.name ) {
node.setAttribute( 'name', data.name );
}
break;
case 'checkbox':
case 'radio':
node = document.createElement( 'div' );
if( data.list ) {
for( i = 0; i < data.list.length; ++i ) {
var cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if( current.type === 'header' ) {
// inline hack
cur_div = node.appendChild( document.createElement( 'h6' ) );
cur_div.appendChild( document.createTextNode( current.label ) );
if( current.tooltip ) {
Morebits.quickForm.element.generateTooltip( cur_div , current );
}
continue;
}
cur_div = node.appendChild( document.createElement( 'div' ) );
subnode = cur_div.appendChild( document.createElement( 'input' ) );
subnode.values = current.value;
subnode.setAttribute( 'value', current.value );
subnode.setAttribute( 'name', current.name || data.name );
subnode.setAttribute( 'type', data.type );
subnode.setAttribute( 'id', cur_id );
if( current.checked ) {
subnode.setAttribute( 'checked', 'checked' );
}
if( current.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.event ) {
subnode.addEventListener( 'change', data.event, false );
} else if ( current.event ) {
subnode.addEventListener( 'change', current.event, true );
}
label = cur_div.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( current.label ) );
label.setAttribute( 'for', cur_id );
if( current.tooltip ) {
Morebits.quickForm.element.generateTooltip( label, current );
}
var event;
if( current.subgroup ) {
var tmpgroup = current.subgroup; // $.extend({}, current.subgroup); really needed?
if( ! $.isArray( tmpgroup ) ) {
tmpgroup = [ tmpgroup ];
}
var subgroupRaw = new Morebits.quickForm.element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each( tmpgroup, function( idx, el ) {
if( ! el.type ) {
el.type = data.type;
}
el.name = (current.name || data.name) + '.' + el.name;
subgroupRaw.append( el );
} );
var subgroup = subgroupRaw.render( cur_id );
subgroup.className = "quickformSubgroup";
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if( e.target.checked ) {
e.target.parentNode.appendChild( e.target.subgroup );
if( e.target.type === 'radio' ) {
var name = e.target.name;
if( e.target.form.names[name] !== undefined ) {
e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup );
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild( e.target.subgroup );
}
};
subnode.addEventListener( 'change', event, true );
if( current.checked ) {
subnode.parentNode.appendChild( subgroup );
}
} else if( data.type === 'radio' ) {
event = function(e) {
if( e.target.checked ) {
var name = e.target.name;
if( e.target.form.names[name] !== undefined ) {
e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup );
}
delete e.target.form.names[name];
}
};
subnode.addEventListener( 'change', event, true );
}
}
}
break;
case 'input':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( data.label ) );
label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'input' ) );
if( data.value ) {
subnode.setAttribute( 'value', data.value );
}
subnode.setAttribute( 'name', data.name );
subnode.setAttribute( 'id', id );
subnode.setAttribute( 'type', 'text' );
if( data.size ) {
subnode.setAttribute( 'size', data.size );
}
if( data.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.readonly ) {
subnode.setAttribute( 'readonly', 'readonly' );
}
if( data.maxlength ) {
subnode.setAttribute( 'maxlength', data.maxlength );
}
if( data.event ) {
subnode.addEventListener( 'keyup', data.event, false );
}
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement( 'div' );
label = node.appendChild( document.createElement( 'h5' ) );
label.appendChild( document.createTextNode( data.label ) );
var listNode = node.appendChild( document.createElement( 'div' ) );
var more = this.compute( {
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
var area = e.target.area;
var new_node = new Morebits.quickForm.element( e.target.sublist );
e.target.area.appendChild( new_node.render() );
if( ++e.target.counter >= e.target.max ) {
e.target.setAttribute( 'disabled', 'disabled' );
}
e.stopPropagation();
}
} );
node.appendChild( more[0] );
var moreButton = more[1];
var sublist = {
type: '_dyninput_element',
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size,
remove: false,
maxlength: data.maxlength,
event: data.event
};
for( i = 0; i < min; ++i ) {
var elem = new Morebits.quickForm.element( sublist );
listNode.appendChild( elem.render() );
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_element': // Private, similar to normal input
node = document.createElement( 'div' );
if( data.label ) {
label = node.appendChild( document.createElement( 'label' ) );
label.appendChild( document.createTextNode( data.label ) );
label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'input' ) );
if( data.value ) {
subnode.setAttribute( 'value', data.value );
}
subnode.setAttribute( 'name', data.name );
subnode.setAttribute( 'type', 'text' );
if( data.size ) {
subnode.setAttribute( 'size', data.size );
}
if( data.maxlength ) {
subnode.setAttribute( 'maxlength', data.maxlength );
}
if( data.event ) {
subnode.addEventListener( 'keyup', data.event, false );
}
if( data.remove ) {
var remove = this.compute( {
type: 'button',
label: 'remove',
event: function(e) {
var list = e.target.listnode;
var node = e.target.inputnode;
var more = e.target.morebutton;
list.removeChild( node );
--more.counter;
more.removeAttribute( 'disabled' );
e.stopPropagation();
}
} );
node.appendChild( remove[0] );
var removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case 'hidden':
node = document.createElement( 'input' );
node.setAttribute( 'type', 'hidden' );
node.values = data.value;
node.setAttribute( 'value', data.value );
node.setAttribute( 'name', data.name );
break;
case 'header':
node = document.createElement( 'h5' );
node.appendChild( document.createTextNode( data.label ) );
break;
case 'div':
node = document.createElement( 'div' );
if (data.name) {
node.setAttribute( 'name', data.name );
}
if (data.label) {
if ( ! $.isArray( data.label ) ) {
data.label = [ data.label ];
}
var result = document.createElement( 'span' );
result.className = 'quickformDescription';
for( i = 0; i < data.label.length; ++i ) {
if( typeof data.label[i] === 'string' ) {
result.appendChild( document.createTextNode( data.label[i] ) );
} else if( data.label[i] instanceof Element ) {
result.appendChild( data.label[i] );
}
}
node.appendChild( result );
}
break;
case 'submit':
node = document.createElement( 'span' );
childContainder = node.appendChild(document.createElement( 'input' ));
childContainder.setAttribute( 'type', 'submit' );
if( data.label ) {
childContainder.setAttribute( 'value', data.label );
}
childContainder.setAttribute( 'name', data.name || 'submit' );
if( data.disabled ) {
childContainder.setAttribute( 'disabled', 'disabled' );
}
break;
case 'button':
node = document.createElement( 'span' );
childContainder = node.appendChild(document.createElement( 'input' ));
childContainder.setAttribute( 'type', 'button' );
if( data.label ) {
childContainder.setAttribute( 'value', data.label );
}
childContainder.setAttribute( 'name', data.name );
if( data.disabled ) {
childContainder.setAttribute( 'disabled', 'disabled' );
}
if( data.event ) {
childContainder.addEventListener( 'click', data.event, false );
}
break;
case 'textarea':
node = document.createElement( 'div' );
node.setAttribute( 'id', 'div_' + id );
if( data.label ) {
label = node.appendChild( document.createElement( 'h5' ) );
label.appendChild( document.createTextNode( data.label ) );
// TODO need to nest a <label> tag in here without creating extra vertical space
//label.setAttribute( 'for', id );
}
subnode = node.appendChild( document.createElement( 'textarea' ) );
subnode.setAttribute( 'name', data.name );
if( data.cols ) {
subnode.setAttribute( 'cols', data.cols );
}
if( data.rows ) {
subnode.setAttribute( 'rows', data.rows );
}
if( data.disabled ) {
subnode.setAttribute( 'disabled', 'disabled' );
}
if( data.readonly ) {
subnode.setAttribute( 'readonly', 'readonly' );
}
if( data.value ) {
subnode.value = data.value;
}
break;
default:
throw new Error("Morebits.quickForm: unknown element type " + data.type.toString());
}
if( !childContainder ) {
childContainder = node;
}
if( data.tooltip ) {
Morebits.quickForm.element.generateTooltip( label || node , data );
}
if( data.extra ) {
childContainder.extra = data.extra;
}
if( data.style ) {
childContainder.setAttribute( 'style', data.style );
}
childContainder.setAttribute( 'id', data.id || id );
return [ node, childContainder ];
};
Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip( node, data ) {
$('<span/>', {
'class': 'ui-icon ui-icon-help ui-icon-inline morebits-tooltip'
}).appendTo(node).tipsy({
'fallback': data.tooltip,
'fade': true,
'gravity': $.fn.tipsy.autoWE,
'html': true,
'delayOut': 250
});
};
/**
* Some utility methods for manipulating quickForms after their creation
* (None of them work for "dyninput" type fields at present)
*
* Morebits.quickForm.getElements(form, fieldName)
* Returns all form elements with a given field name or ID
*
* Morebits.quickForm.getCheckboxOrRadio(elementArray, value)
* Searches the array of elements for a checkbox or radio button with a certain |value| attribute
*
* Morebits.quickForm.getElementContainer(element)
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios
*
* Morebits.quickForm.getElementLabelObject(element)
* Gets the HTML element that contains the label of the given form element (mainly for internal use)
*
* Morebits.quickForm.getElementLabel(element)
* Gets the label text of the element
*
* Morebits.quickForm.setElementLabel(element, labelText)
* Sets the label of the element to the given text
*
* Morebits.quickForm.overrideElementLabel(element, temporaryLabelText)
* Stores the element's current label, and temporarily sets the label to the given text
*
* Morebits.quickForm.resetElementLabel(element)
* Restores the label stored by overrideElementLabel
*
* Morebits.quickForm.setElementVisibility(element, visibility)
* Shows or hides a form element plus its label and tooltip
*
* Morebits.quickForm.setElementTooltipVisibility(element, visibility)
* Shows or hides the "question mark" icon next to a form element
*/
Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) {
var $form = $(form);
var $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
if ($elements.length > 0) {
return $elements.toArray();
}
return null;
};
Morebits.quickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
var found = $.grep(elementArray, function(el) {
return el.value === value;
});
if (found.length > 0) {
return found[0];
}
return null;
};
Morebits.quickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
Morebits.quickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === "button" || element.type === "submit" ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName("legend")[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName("h5")[0];
// for others, the label is the sibling <label> element
} else {
return element.parentNode.getElementsByTagName("label")[0];
}
return null;
};
Morebits.quickForm.getElementLabel = function QuickFormGetElementLabel(element) {
var labelElement = Morebits.quickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
var labelElement = Morebits.quickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute("data-oldlabel")) {
element.setAttribute("data-oldlabel", Morebits.quickForm.getElementLabel(element));
}
return Morebits.quickForm.setElementLabel(element, temporaryLabelText);
};
Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute("data-oldlabel")) {
return Morebits.quickForm.setElementLabel(element, element.getAttribute("data-oldlabel"));
}
return null;
};
Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.quickForm.getElementContainer(element)).find(".morebits-tooltip").toggle(visibility);
};
/**
* **************** HTMLFormElement ****************
*
* getChecked:
* XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better
*
* Returns an array containing the values of elements with the given name, that has it's
* checked property set to true. (i.e. a checkbox or a radiobutton is checked), or select options
* that have selected set to true. (don't try to mix selects with radio/checkboxes, please)
* Type is optional and can specify if either radio or checkbox (for the event
* that both checkboxes and radiobuttons have the same name.
*/
HTMLFormElement.prototype.getChecked = function( name, type ) {
var elements = this.elements[name];
if( !elements ) {
// if the element doesn't exists, return null.
return null;
}
var return_array = [];
var i;
if( elements instanceof HTMLSelectElement ) {
var options = elements.options;
for( i = 0; i < options.length; ++i ) {
if( options[i].selected ) {
if( options[i].values ) {
return_array.push( options[i].values );
} else {
return_array.push( options[i].value );
}
}
}
} else if( elements instanceof HTMLInputElement ) {
if( type && elements.type !== type ) {
return [];
} else if( elements.checked ) {
return [ elements.value ];
}
} else {
for( i = 0; i < elements.length; ++i ) {
if( elements[i].checked ) {
if( type && elements[i].type !== type ) {
continue;
}
if( elements[i].values ) {
return_array.push( elements[i].values );
} else {
return_array.push( elements[i].value );
}
}
}
}
return return_array;
};
/**
* **************** RegExp ****************
*
* RegExp.escape: Will escape a string to be used in a RegExp
*/
RegExp.escape = function( text, space_fix ) {
text = mw.RegExp.escape(text);
// Special MediaWiki escape - underscore/space are often equivalent
if( space_fix ) {
text = text.replace( / |_/g, '[_ ]' );
}
return text;
};
/**
* **************** Morebits.bytes ****************
* Utility object for formatting byte values
*/
Morebits.bytes = function( value ) {
if( typeof value === 'string' ) {
var res = /(\d+) ?(\w?)(i?)B?/.exec( value );
var number = res[1];
var mag = res[2];
var si = res[3];
if( !number ) {
this.number = 0;
return;
}
if( !si ) {
this.value = number * Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 );
} else {
this.value = number * Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 );
}
} else {
this.value = value;
}
};
Morebits.bytes.magnitudes = {
'': 0,
'K': 1,
'M': 2,
'G': 3,
'T': 4,
'P': 5,
'E': 6,
'Z': 7,
'Y': 8
};
Morebits.bytes.rmagnitudes = {
0: '',
1: 'K',
2: 'M',
3: 'G',
4: 'T',
5: 'P',
6: 'E',
7: 'Z',
8: 'Y'
};
Morebits.bytes.prototype.valueOf = function() {
return this.value;
};
Morebits.bytes.prototype.toString = function( magnitude ) {
var tmp = this.value;
if( magnitude ) {
var si = /i/.test(magnitude);
var mag = magnitude.replace( /.*?(\w)i?B?.*/g, '$1' );
if( si ) {
tmp /= Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 );
} else {
tmp /= Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 );
}
if( parseInt( tmp, 10 ) !== tmp ) {
tmp = Number( tmp ).toPrecision( 4 );
}
return tmp + ' ' + mag + (si?'i':'') + 'B';
} else {
// si per default
var current = 0;
while( tmp >= 1024 ) {
tmp /= 1024;
++current;
}
tmp = this.value / Math.pow( 2, current * 10 );
if( parseInt( tmp, 10 ) !== tmp ) {
tmp = Number( tmp ).toPrecision( 4 );
}
return tmp + ' ' + Morebits.bytes.rmagnitudes[current] + ( current > 0 ? 'iB' : 'B' );
}
};
/**
* **************** String; Morebits.string ****************
*/
if (!String.prototype.trimLeft) {
String.prototype.trimLeft = function stringPrototypeLtrim( chars ) {
chars = chars || "\\s";
return this.replace( new RegExp("^[" + chars + "]+", "g"), "" );
};
}
if (!String.prototype.trimRight) {
String.prototype.trimRight = function stringPrototypeRtrim( chars ) {
chars = chars || "\\s";
return this.replace( new RegExp("[" + chars + "]+$", "g"), "" );
};
}
if (!String.prototype.trim) {
String.prototype.trim = function stringPrototypeTrim( chars ) {
return this.trimRight(chars).trimLeft(chars);
};
}
// Helper functions to change case of a string
Morebits.string = {
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 );
},
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.substr( 0, 1 ).toLowerCase() + str.substr( 1 );
},
splitWeightedByKeys: function( str, start, end, skip ) {
if( start.length !== end.length ) {
throw new Error( 'start marker and end marker must be of the same length' );
}
var level = 0;
var initial = null;
var result = [];
if( ! $.isArray( skip ) ) {
if( skip === undefined ) {
skip = [];
} else if( typeof skip === 'string' ) {
skip = [ skip ];
} else {
throw new Error( "non-applicable skip parameter" );
}
}
for( var i = 0; i < str.length; ++i ) {
for( var j = 0; j < skip.length; ++j ) {
if( str.substr( i, skip[j].length ) === skip[j] ) {
i += skip[j].length - 1;
continue;
}
}
if( str.substr( i, start.length ) === start ) {
if( initial === null ) {
initial = i;
}
++level;
i += start.length - 1;
} else if( str.substr( i, end.length ) === end ) {
--level;
i += end.length - 1;
}
if( !level && initial !== null ) {
result.push( str.substring( initial, i + 1 ) );
initial = null;
}
}
return result;
}
};
/**
* **************** Morebits.array ****************
*
* uniq(arr): returns a copy of the array with duplicates removed
*
* dups(arr): returns a copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain
*
* chunk(arr, size): breaks up |arr| into smaller arrays of length |size|, and
* returns an array of these "chunked" arrays
*/
Morebits.array = {
uniq: function(arr) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.uniq";
}
var result = [];
for( var i = 0; i < arr.length; ++i ) {
var current = arr[i];
if( result.indexOf( current ) === -1 ) {
result.push( current );
}
}
return result;
},
dups: function(arr) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.dups";
}
var uniques = [];
var result = [];
for( var i = 0; i < arr.length; ++i ) {
var current = arr[i];
if( uniques.indexOf( current ) === -1 ) {
uniques.push( current );
} else {
result.push( current );
}
}
return result;
},
chunk: function( arr, size ) {
if ( ! $.isArray( arr ) ) {
throw "A non-array object passed to Morebits.array.chunk";
}
if( typeof size !== 'number' || size <= 0 ) { // pretty impossible to do anything :)
return [ arr ]; // we return an array consisting of this array.
}
var result = [];
var current;
for( var i = 0; i < arr.length; ++i ) {
if( i % size === 0 ) { // when 'i' is 0, this is always true, so we start by creating one.
current = [];
result.push( current );
}
current.push( arr[i] );
}
return result;
}
};
/**
* **************** Morebits.getPageAssociatedUser ****************
* Get the user associated with the currently-viewed page.
* Currently works on User:, User talk:, Special:Contributions.
*/
Morebits.getPageAssociatedUser = function(){
var thisNamespaceId = mw.config.get('wgNamespaceNumber');
if ( thisNamespaceId === 2 /* User: */ || thisNamespaceId === 3 /* User talk: */ ) {
return mw.config.get('wgTitle').split( '/' )[0]; // only first part before any slashes, to work on subpages
}
if ( thisNamespaceId === -1 /* Special: */ && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) {
return mw.config.get("wgRelevantUserName");
}
return false;
};
/**
* **************** Morebits.unbinder ****************
* Used by Morebits.wikitext.page.commentOutImage
*/
Morebits.unbinder = function Unbinder( string ) {
if( typeof string !== 'string' ) {
throw new Error( "not a string" );
}
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
}
Morebits.unbinder.prototype = {
unbind: function UnbinderUnbind( prefix, postfix ) {
var re = new RegExp( prefix + '(.*?)' + postfix, 'g' );
this.content = this.content.replace( re, Morebits.unbinder.getCallback( this ) );
},
rebind: function UnbinderRebind() {
var content = this.content;
content.self = this;
for( var current in this.history ) {
if( this.history.hasOwnProperty( current ) ) {
content = content.replace( current, this.history[current] );
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
Morebits.unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback( match , a , b ) {
var current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/**
* **************** Date ****************
* Helper functions to get the month as a string instead of a number
*
* Normally it is poor form to play with prototypes of primitive types, but it
* is fairly unlikely that anyone will iterate over a Date object.
*/
Date.monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
Date.monthNamesAbbrev = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
Date.prototype.getMonthName = function() {
return Date.monthNames[ this.getMonth() ];
};
Date.prototype.getMonthNameAbbrev = function() {
return Date.monthNamesAbbrev[ this.getMonth() ];
};
Date.prototype.getUTCMonthName = function() {
return Date.monthNames[ this.getUTCMonth() ];
};
Date.prototype.getUTCMonthNameAbbrev = function() {
return Date.monthNamesAbbrev[ this.getUTCMonth() ];
};
/**
* **************** Morebits.wikipedia ****************
* English Wikipedia-specific objects
*/
Morebits.wikipedia = {};
Morebits.wikipedia.namespaces = {
'-2': 'Media',
'-1': 'Special',
'0': '',
'1': 'Talk',
'2': 'User',
'3': 'User talk',
'4': 'Project',
'5': 'Project talk',
'6': 'File',
'7': 'File talk',
'8': 'MediaWiki',
'9': 'MediaWiki talk',
'10': 'Template',
'11': 'Template talk',
'12': 'Help',
'13': 'Help talk',
'14': 'Category',
'15': 'Category talk',
'100': 'Portal',
'101': 'Portal talk',
'108': 'Book',
'109': 'Book talk'
};
Morebits.wikipedia.namespacesFriendly = {
'0': '(Article)',
'1': 'Talk',
'2': 'User',
'3': 'User talk',
'4': 'Wikipedia',
'5': 'Wikipedia talk',
'6': 'File',
'7': 'File talk',
'8': 'MediaWiki',
'9': 'MediaWiki talk',
'10': 'Template',
'11': 'Template talk',
'12': 'Help',
'13': 'Help talk',
'14': 'Category',
'15': 'Category talk',
'100': 'Portal',
'101': 'Portal talk',
'108': 'Book',
'109': 'Book talk'
};
/**
* **************** Morebits.wiki ****************
* Various objects for wiki editing and API access
*/
Morebits.wiki = {};
// Analyzes the HTML of the current page (i.e. no AJAX requests) to determine if it
// is a redirect or soft redirect
Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() {
return !!($("span.redirectText").length > 0 || document.getElementById("softredirect"));
};
/**
* **************** Morebits.wiki.actionCompleted ****************
*
* Use of Morebits.wiki.actionCompleted():
* Every call to Morebits.wiki.api.post() results in the dispatch of
* an asynchronous callback. Each callback can in turn
* make an additional call to Morebits.wiki.api.post() to continue a
* processing sequence. At the conclusion of the final callback
* of a processing sequence, it is not possible to simply return to the
* original caller because there is no call stack leading back to
* the original context. Instead, Morebits.wiki.actionCompleted.event() is
* called to display the result to the user and to perform an optional
* page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event()
* is managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.api call and decremented
* after the completion of a callback function. If a callback function
* does not create a new Morebits.wiki.api object before exiting, it is the
* final step in the processing chain and Morebits.wiki.actionCompleted.event()
* will then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback function.
* This is used for batch operations. The end of a batch is signaled by calling
* Morebits.wiki.removeCheckpoint().
*/
Morebits.wiki.numberOfActionsLeft = 0;
Morebits.wiki.nbrOfCheckpointsLeft = 0;
Morebits.wiki.actionCompleted = function( self ) {
if( --Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0 ) {
Morebits.wiki.actionCompleted.event( self );
}
};
// Change per action wanted
Morebits.wiki.actionCompleted.event = function() {
new Morebits.status( Morebits.wiki.actionCompleted.notice, Morebits.wiki.actionCompleted.postfix, 'info' );
if( Morebits.wiki.actionCompleted.redirect ) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if( !( (/^\w+\:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl( Morebits.wiki.actionCompleted.redirect );
if( Morebits.wiki.actionCompleted.followRedirect === false ) {
Morebits.wiki.actionCompleted.redirect += "?redirect=no";
}
}
window.setTimeout( function() { window.location = Morebits.wiki.actionCompleted.redirect; }, Morebits.wiki.actionCompleted.timeOut );
}
};
Morebits.wiki.actionCompleted.timeOut = ( typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut );
Morebits.wiki.actionCompleted.redirect = null;
Morebits.wiki.actionCompleted.notice = 'Action';
Morebits.wiki.actionCompleted.postfix = 'completed';
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
Morebits.wiki.removeCheckpoint = function() {
if( --Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0 ) {
Morebits.wiki.actionCompleted.event();
}
};
/**
* **************** Morebits.wiki.api ****************
* An easy way to talk to the MediaWiki API.
*
* Constructor parameters:
* currentAction: the current action (required)
* query: the query (required)
* onSuccess: the function to call when request gotten
* statusElement: a Morebits.status object to use for status messages (optional)
* onError: the function to call if an error occurs (optional)
*/
Morebits.wiki.api = function( currentAction, query, onSuccess, statusElement, onError ) {
this.currentAction = currentAction;
this.query = query;
this.query.format = 'xml';
this.query.assert = 'user';
this.onSuccess = onSuccess;
this.onError = onError;
if( statusElement ) {
this.statelem = statusElement;
this.statelem.status( currentAction );
} else {
this.statelem = new Morebits.status( currentAction );
}
};
Morebits.wiki.api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
responseXML: null,
setParent: function(parent) { this.parent = parent; }, // keep track of parent object for callbacks
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
// post(): carries out the request
// do not specify a parameter unless you really really want to give jQuery some extra parameters
post: function( callerAjaxParameters ) {
++Morebits.wiki.numberOfActionsLeft;
var ajaxparams = $.extend( {}, {
context: this,
type: 'POST',
url: mw.util.wikiScript('api'),
data: Morebits.queryString.create(this.query),
datatype: 'xml',
headers: {
'Api-User-Agent': morebitsWikiApiUserAgent
},
success: function(xml, statusText, jqXHR) {
this.statusText = statusText;
this.responseXML = xml;
this.errorCode = $(xml).find('error').attr('code');
this.errorText = $(xml).find('error').attr('info');
if (typeof this.errorCode === "string") {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
this.returnError();
return;
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call( this.parent, this );
} else {
this.statelem.info("done");
}
Morebits.wiki.actionCompleted();
},
// only network and server errors reach here – complaints from the API itself are caught in success()
error: function(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
this.returnError();
}
}, callerAjaxParameters );
return $.ajax( ajaxparams ); // the return value should be ignored, unless using callerAjaxParameters with |async: false|
},
returnError: function() {
this.statelem.error( this.errorText );
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call( this.parent, this );
}
// don't complete the action so that the error remains displayed
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() {
return this.responseXML;
}
};
// Custom user agent header, used by WMF for server-side logging
// See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html
var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])';
// Sets the custom user agent header
Morebits.wiki.api.setApiUserAgent = function( ua ) {
morebitsWikiApiUserAgent = ( ua ? ua + ' ' : '' ) + 'morebits.js/2.0 ([[w:WT:TW]])';
};
/**
* **************** Morebits.wiki.page ****************
* Uses the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set Morebits.wiki.actionCompleted.notice and Morebits.wiki.actionCompleted.redirect
* before the first call to Morebits.wiki.page.load().
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.page prototype method using this reference.
*
*
* NOTE: This list of member functions is incomplete.
*
* Constructor: Morebits.wiki.page(pageName, currentAction)
* pageName - the name of the page, prefixed by the namespace (if any)
* (for the current page, use mw.config.get('wgPageName'))
* currentAction - a string describing the action about to be undertaken (optional)
*
* load(onSuccess, onFailure): Loads the text for the page
* onSuccess - callback function which is called when the load has succeeded
* onFailure - callback function which is called when the load fails (optional)
* XXX onFailure for load() is not yet implemented – do we need it? -- UncleDouggie
* probably not -- TTO
*
* save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load().
* onSuccess - callback function which is called when the save has succeeded (optional)
* onFailure - callback function which is called when the save fails (optional)
* Warning: Calling save() can result in additional calls to the previous load() callbacks to
* recover from edit conflicts!
* In this case, callers must make the same edit to the new pageText and reinvoke save().
* This behavior can be disabled with setMaxConflictRetries(0).
*
* append(onSuccess, onFailure): Adds the text provided via setAppendText() to the end of the page.
* Does not require calling load() first.
* onSuccess - callback function which is called when the method has succeeded (optional)
* onFailure - callback function which is called when the method fails (optional)
*
* prepend(onSuccess, onFailure): Adds the text provided via setPrependText() to the start of the page.
* Does not require calling load() first.
* onSuccess - callback function which is called when the method has succeeded (optional)
* onFailure - callback function which is called when the method fails (optional)
*
* getPageName(): returns a string containing the name of the loaded page, including the namespace
*
* getPageText(): returns a string containing the text of the page after a successful load()
*
* setPageText(pageText)
* pageText - string containing the updated page text that will be saved when save() is called
*
* setAppendText(appendText)
* appendText - string containing the text that will be appended to the page when append() is called
*
* setPrependText(prependText)
* prependText - string containing the text that will be prepended to the page when prepend() is called
*
* setEditSummary(summary)
* summary - string containing the text of the edit summary that will be used when save() is called
*
* setMinorEdit(minorEdit)
* minorEdit is a boolean value:
* true - When save is called, the resulting edit will be marked as "minor".
* false - When save is called, the resulting edit will not be marked as "minor". (default)
*
* setPageSection(pageSection)
* pageSection - integer specifying the section number to load or save. The default is |null|, which means
* that the entire page will be retrieved.
*
* setMaxConflictRetries(maxRetries)
* maxRetries - number of retries for save errors involving an edit conflict or loss of edit token
* default: 2
*
* setMaxRetries(maxRetries)
* maxRetries - number of retries for save errors not involving an edit conflict or loss of edit token
* default: 2
*
* setCallbackParameters(callbackParameters)
* callbackParameters - an object for use in a callback function
*
* getCallbackParameters(): returns the object previous set by setCallbackParameters()
*
* Callback notes: callbackParameters is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback function.
* Callers must ensure that any changes to the callbackParameters object
* within a load() callback still permit a proper re-entry into the
* load() callback if an edit conflict is detected upon calling save().
*
* getStatusElement(): returns the Status element created by the constructor
*
* setFollowRedirect(followRedirect)
* followRedirect is a boolean value:
* true - a maximum of one redirect will be followed.
* In the event of a redirect, a message is displayed to the user and
* the redirect target can be retrieved with getPageName().
* false - the requested pageName will be used without regard to any redirect. (default)
*
* setWatchlist(watchlistOption)
* watchlistOption is a boolean value:
* true - page will be added to the user's watchlist when save() is called
* false - watchlist status of the page will not be changed (default)
*
* setWatchlistFromPreferences(watchlistOption)
* watchlistOption is a boolean value:
* true - page watchlist status will be set based on the user's
* preference settings when save() is called
* false - watchlist status of the page will not be changed (default)
*
* Watchlist notes:
* 1. The MediaWiki API value of 'unwatch', which explicitly removes the page from the
* user's watchlist, is not used.
* 2. If both setWatchlist() and setWatchlistFromPreferences() are called,
* the last call takes priority.
* 3. Twinkle modules should use the appropriate preference to set the watchlist options.
* 4. Most Twinkle modules use setWatchlist().
* setWatchlistFromPreferences() is only needed for the few Twinkle watchlist preferences
* that accept a string value of 'default'.
*
* setCreateOption(createOption)
* createOption is a string value:
* 'recreate' - create the page if it does not exist, or edit it if it exists
* 'createonly' - create the page if it does not exist, but return an error if it
* already exists
* 'nocreate' - don't create the page, only edit it if it already exists
* null - create the page if it does not exist, unless it was deleted in the moment
* between retrieving the edit token and saving the edit (default)
*
* exists(): returns true if the page existed on the wiki when it was last loaded
*
* lookupCreator(onSuccess): Retrieves the username of the user who created the page
* onSuccess - callback function which is called when the username is found
* within the callback, the username can be retrieved using the getCreator() function
*
* getCreator(): returns the user who created the page following lookupCreator()
*
* patrol(): marks the page as patrolled (only when "rcid" is present in the query string)
*
* move(onSuccess, onFailure): Moves a page to another title
*
* deletePage(onSuccess, onFailure): Deletes a page (for admins only)
*
*/
/**
* Call sequence for common operations (optional final user callbacks not shown):
*
* Edit current contents of a page (no edit conflict):
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Edit current contents of a page (with edit conflict):
* .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() ->
* ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Append to a page (similar for prepend):
* .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()
*
* Notes:
* 1. All functions following Morebits.wiki.api.post() are invoked asynchronously
* from the jQuery AJAX library.
* 2. The sequence for append/prepend could be slightly shortened, but it would require
* significant duplication of code for little benefit.
*/
Morebits.wiki.page = function(pageName, currentAction) {
if (!currentAction) {
currentAction = 'Opening page "' + pageName + '"';
}
/**
* Private context variables
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*/
var ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
callbackParameters: null,
statusElement: new Morebits.status(currentAction),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
createOption: null,
minorEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
watchlistOption: 'nochange',
creator: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
editToken: null,
loadTime: null,
lastEditTime: null,
revertCurID: null,
revertUser: null,
fullyProtected: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreatorSuccess: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
saveApi: null,
lookupCreatorApi: null,
moveApi: null,
moveProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
var emptyFunction = function() { };
/**
* Public interface accessors
*/
this.getPageName = function() {
return ctx.pageName;
};
this.getPageText = function() {
return ctx.pageText;
};
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
this.setMaxConflictRetries = function(maxRetries) {
ctx.maxConflictRetries = maxRetries;
};
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
this.getCreator = function() {
return ctx.creator;
};
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
this.getRevisionUser = function() {
return ctx.revertUser;
};
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry };
};
this.getStatusElement = function() {
return ctx.statusElement;
};
this.setFollowRedirect = function(followRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error("Internal error: cannot change redirect setting after the page has been loaded!");
return;
}
ctx.followRedirect = followRedirect;
};
this.setWatchlist = function(flag) {
if (flag) {
ctx.watchlistOption = 'watch';
} else {
ctx.watchlistOption = 'nochange';
}
};
this.setWatchlistFromPreferences = function(flag) {
if (flag) {
ctx.watchlistOption = 'preferences';
} else {
ctx.watchlistOption = 'nochange';
}
};
this.exists = function() {
return ctx.pageExists;
};
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error("Internal error: no onSuccess callback provided to load()!");
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsInGroup('sysop')) {
ctx.loadQuery.inprop = 'protection';
}
ctx.loadApi = new Morebits.wiki.api("Retrieving page...", ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
// Save updated .pageText to Wikipedia
// Only valid after successful .load()
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.pageLoaded) {
ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!");
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: edit summary not set before save!");
ctx.onSaveFailure(this);
return;
}
if (ctx.fullyProtected && !confirm('You are about to make an edit to the fully protected page "' + ctx.pageName +
(ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) +
'. \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) {
ctx.statusElement.error("Edit to fully protected page was aborted.");
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
var query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: ctx.editToken,
watchlist: ctx.watchlistOption
};
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
switch (ctx.editMode) {
case 'append':
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default:
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) {
query[ctx.createOption] = '';
}
ctx.saveApi = new Morebits.wiki.api( "Saving page...", query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.lookupCreator = function(onSuccess) {
if (!onSuccess) {
ctx.statusElement.error("Internal error: no onSuccess callback provided to lookupCreator()!");
return;
}
ctx.onLookupCreatorSuccess = onSuccess;
var query = {
'action': 'query',
'prop': 'revisions',
'titles': ctx.pageName,
'rvlimit': 1,
'rvprop': 'user',
'rvdir': 'newer'
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreatorApi = new Morebits.wiki.api("Retrieving page creator information", query, fnLookupCreatorSuccess, ctx.statusElement);
ctx.lookupCreatorApi.setParent(this);
ctx.lookupCreatorApi.post();
};
this.patrol = function() {
// look for rcid in querystring; if not, we won't have a patrol token, so no point trying
if (!Morebits.queryString.exists("rcid")) {
return;
}
var rcid = Morebits.queryString.get("rcid");
// extract patrol token from "Mark page as patrolled" link on page
var patrollinkmatch = /token=(.+)%2B%5C$/.exec($(".patrollink a").attr("href"));
if (patrollinkmatch) {
var patroltoken = patrollinkmatch[1] + "+\\";
var patrolstat = new Morebits.status("Marking page as patrolled");
var wikipedia_api = new Morebits.wiki.api("doing...", {
title: ctx.pageName,
action: 'markpatrolled',
rcid: rcid,
token: patroltoken
}, null, patrolstat);
wikipedia_api.post({
type: 'GET',
url: mw.util.wikiScript('index'),
datatype: 'text' // we don't really care about the response
});
}
};
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error("Internal error: revision ID to revert to was not set before revert!");
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: move reason not set before move (use setEditSummary function)!");
ctx.onMoveFailure(this);
return;
}
if (!ctx.moveDestination) {
ctx.statusElement.error("Internal error: destination page name was not set before move!");
ctx.onMoveFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
if (Morebits.userIsInGroup('sysop')) {
query.inprop = 'protection';
}
ctx.moveApi = new Morebits.wiki.api("retrieving move token...", query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
};
// |delete| is a reserved word in some flavours of JS
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot delete page: only admins can do that");
ctx.onDeleteFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: delete reason not set before delete (use setEditSummary function)!");
ctx.onDeleteFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
inprop: 'protection',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
};
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot protect page: only admins can do that");
ctx.onProtectFailure(this);
return;
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error("Internal error: you must set edit and/or move and/or create protection before calling protect()!");
ctx.onProtectFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: protection reason not set before protect (use setEditSummary function)!");
ctx.onProtectFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info',
inprop: 'protection',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.protectApi = new Morebits.wiki.api("retrieving protect token...", query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
// apply FlaggedRevs protection-style settings
// only works where $wgFlaggedRevsProtection = true (i.e. where FlaggedRevs
// settings appear on the wiki's "protect" tab)
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsInGroup('sysop')) {
ctx.statusElement.error("Cannot apply FlaggedRevs settings: only admins can do that");
ctx.onStabilizeFailure(this);
return;
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error("Internal error: you must set flaggedRevs before calling stabilize()!");
ctx.onStabilizeFailure(this);
return;
}
if (!ctx.editSummary) {
ctx.statusElement.error("Internal error: reason not set before calling stabilize() (use setEditSummary function)!");
ctx.onStabilizeFailure(this);
return;
}
var query = {
action: 'query',
prop: 'info|flagged',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName
};
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.stabilizeApi = new Morebits.wiki.api("retrieving stabilize token...", query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
};
/**
* Private member functions
*
* These are not exposed outside
*/
// callback from loadSuccess() for append() and prepend() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
var xml = ctx.loadApi.getXML();
if ( !fnCheckPageName(xml, ctx.onLoadFailure) ) {
return; // abort
}
ctx.pageExists = ($(xml).find('page').attr('missing') !== "");
if (ctx.pageExists) {
ctx.pageText = $(xml).find('rev').text();
} else {
ctx.pageText = ''; // allow for concatenation, etc.
}
// extract protection info, to alert admins when they are about to edit a protected page
if (Morebits.userIsInGroup('sysop')) {
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop') {
ctx.fullyProtected = editprot.attr('expiry');
} else {
ctx.fullyProtected = false;
}
}
ctx.editToken = $(xml).find('tokens').attr('csrftoken');
if (!ctx.editToken)
{
ctx.statusElement.error("Failed to retrieve edit token.");
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = $(xml).find('api').attr('curtimestamp');
if (!ctx.loadTime)
{
ctx.statusElement.error("Failed to retrieve start timestamp.");
ctx.onLoadFailure(this);
return;
}
ctx.lastEditTime = $(xml).find('page').attr('touched');
if (ctx.editMode === 'revert') {
ctx.revertCurID = $(xml).find('rev').attr('revid');
if (!ctx.revertCurID) {
ctx.statusElement.error("Failed to retrieve current revision ID.");
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = $(xml).find('rev').attr('user');
if (!ctx.revertUser) {
if ($(xml).find('rev').attr('userhidden') === "") { // username was RevDel'd or oversighted
ctx.revertUser = "<username hidden>";
} else {
ctx.statusElement.error("Failed to retrieve user who made the revision.");
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = "[[Help:Revert|Reverted]] to revision " + ctx.revertOldID + " by " + ctx.revertUser + ": " + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(xml, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
// check for invalid titles
if ( $(xml).find('page').attr('invalid') === "" ) {
ctx.statusElement.error("The page title is invalid: " + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
if ( $(xml).find('page').attr('title') ) {
var resolvedName = $(xml).find('page').attr('title');
// only notify user for redirects, not normalization
if ( $(xml).find('redirects').length > 0 ) {
Morebits.status.info("Info", "Redirected from " + ctx.pageName + " to " + resolvedName );
}
ctx.pageName = resolvedName; // always update in case of normalization
}
else {
// could be a circular redirect or other problem
ctx.statusElement.error("Could not resolve redirects for: " + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/revert modes
var xml = ctx.saveApi.getXML();
// see if the API thinks we were successful
if ($(xml).find('edit').attr('result') === "Success") {
// real success
// default on success action - display link for edited page
var link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName) );
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['completed (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki
// Wikimedia wikis should only return spam blacklist errors and captchas
var blacklist = $(xml).find('edit').attr('spamblacklist');
if (blacklist) {
var code = document.createElement('code');
code.style.fontFamily = "monospace";
code.appendChild(document.createTextNode(blacklist));
ctx.statusElement.error(['Could not save the page because the URL ', code, ' is on the spam blacklist.']);
}
else if ( $(xml).find('captcha').length > 0 ) {
ctx.statusElement.error("Could not save the page because the wiki server wanted you to fill out a CAPTCHA.");
}
else {
ctx.statusElement.error("Unknown error received from API while saving page");
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
var errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if ( errorCode === "editconflict" && ctx.conflictRetries++ < ctx.maxConflictRetries ) {
// edit conflicts can occur when the page needs to be purged from the server cache
var purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement);
var result = purgeApi.post( { async: false } ); // just wait for it, result is for debugging
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info("Edit conflict detected, reapplying edit");
ctx.loadApi.post(); // reload the page and reapply the edit
// check for loss of edit token
// it's impractical to request a new token here, so invoke edit conflict logic when this happens
} else if ( errorCode === "notoken" && ctx.conflictRetries++ < ctx.maxConflictRetries ) {
ctx.statusElement.info("Edit token is invalid, retrying");
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.loadApi.post(); // reload
// check for network or server error
} else if ( errorCode === "undefined" && ctx.retries++ < ctx.maxRetries ) {
// the error might be transient, so try again
ctx.statusElement.info("Save failed, retrying");
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.saveApi.post(); // give it another go!
// hard error, give up
} else {
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
if ( errorCode === "protectedpage" ) {
ctx.statusElement.error( "Failed to save edit: Page is fully protected" );
} else {
ctx.statusElement.error( "Failed to save edit: " + ctx.saveApi.getErrorText() );
}
ctx.editMode = 'all'; // cancel append/prepend/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
var fnLookupCreatorSuccess = function() {
var xml = ctx.lookupCreatorApi.getXML();
if ( !fnCheckPageName(xml) ) {
return; // abort
}
ctx.creator = $(xml).find('rev').attr('user');
if (!ctx.creator) {
ctx.statusElement.error("Could not find name of page creator");
return;
}
ctx.onLookupCreatorSuccess(this);
};
var fnProcessMove = function() {
var xml = ctx.moveApi.getXML();
if ($(xml).find('page').attr('missing') === "") {
ctx.statusElement.error("Cannot move the page, because it no longer exists");
ctx.onMoveFailure(this);
return;
}
// extract protection info
if (Morebits.userIsInGroup('sysop')) {
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to move the fully protected page "' + ctx.pageName +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
'. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) {
ctx.statusElement.error("Move of fully protected page was aborted.");
ctx.onMoveFailure(this);
return;
}
}
var moveToken = $(xml).find('tokens').attr('csrftoken');
if (!moveToken) {
ctx.statusElement.error("Failed to retrieve move token.");
ctx.onMoveFailure(this);
return;
}
var query = {
'action': 'move',
'from': $(xml).find('page').attr('title'),
'to': ctx.moveDestination,
'token': moveToken,
'reason': ctx.editSummary
};
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true'; // XXX don't know whether this works for non-admins
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.api("moving page...", query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessDelete = function() {
var xml = ctx.deleteApi.getXML();
if ($(xml).find('page').attr('missing') === "") {
ctx.statusElement.error("Cannot delete the page, because it no longer exists");
ctx.onDeleteFailure(this);
return;
}
// extract protection info
var editprot = $(xml).find('pr[type="edit"]');
if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to delete the fully protected page "' + ctx.pageName +
(editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) +
'. \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) {
ctx.statusElement.error("Deletion of fully protected page was aborted.");
ctx.onDeleteFailure(this);
return;
}
var deleteToken = $(xml).find('tokens').attr('csrftoken');
if (!deleteToken) {
ctx.statusElement.error("Failed to retrieve delete token.");
ctx.onDeleteFailure(this);
return;
}
var query = {
'action': 'delete',
'title': $(xml).find('page').attr('title'),
'token': deleteToken,
'reason': ctx.editSummary
};
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
var fnProcessProtect = function() {
var xml = ctx.protectApi.getXML();
var missing = ($(xml).find('page').attr('missing') === "");
if (((ctx.protectEdit || ctx.protectMove) && missing)) {
ctx.statusElement.error("Cannot protect the page, because it no longer exists");
ctx.onProtectFailure(this);
return;
}
if (ctx.protectCreate && !missing) {
ctx.statusElement.error("Cannot create protect the page, because it already exists");
ctx.onProtectFailure(this);
return;
}
// TODO cascading protection not possible on edit<sysop
var protectToken = $(xml).find('tokens').attr('csrftoken');
if (!protectToken) {
ctx.statusElement.error("Failed to retrieve protect token.");
ctx.onProtectFailure(this);
return;
}
// fetch existing protection levels
var prs = $(xml).find('pr');
var editprot = prs.filter('[type="edit"]');
var moveprot = prs.filter('[type="move"]');
var createprot = prs.filter('[type="create"]');
var protections = [], expirys = [];
// set edit protection level
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
} else if (editprot.length) {
protections.push('edit=' + editprot.attr("level"));
expirys.push(editprot.attr("expiry").replace("infinity", "indefinite"));
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
} else if (moveprot.length) {
protections.push('move=' + moveprot.attr("level"));
expirys.push(moveprot.attr("expiry").replace("infinity", "indefinite"));
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
} else if (createprot.length) {
protections.push('create=' + createprot.attr("level"));
expirys.push(createprot.attr("expiry").replace("infinity", "indefinite"));
}
var query = {
action: 'protect',
title: $(xml).find('page').attr('title'),
token: protectToken,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary
};
if (ctx.protectCascade) {
query.cascade = 'true';
}
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.api("protecting page...", query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
var xml = ctx.stabilizeApi.getXML();
var missing = ($(xml).find('page').attr('missing') === "");
if (missing) {
ctx.statusElement.error("Cannot protect the page, because it no longer exists");
ctx.onStabilizeFailure(this);
return;
}
var stabilizeToken = $(xml).find('tokens').attr('csrftoken');
if (!stabilizeToken) {
ctx.statusElement.error("Failed to retrieve stabilize token.");
ctx.onStabilizeFailure(this);
return;
}
var query = {
action: 'stabilize',
title: $(xml).find('page').attr('title'),
token: stabilizeToken,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
reason: ctx.editSummary
};
if (ctx.watchlistOption === 'watch') {
query.watch = 'true';
}
ctx.stabilizeProcessApi = new Morebits.wiki.api("configuring stabilization settings...", query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
}; // end Morebits.wiki.page
/** Morebits.wiki.page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/**
* **************** Morebits.wiki.preview ****************
* Uses the API to parse a fragment of wikitext and render it as HTML.
*
* Constructor: Morebits.wiki.preview(previewbox, currentAction)
* previewbox - the <div> element that will contain the rendered HTML
*
* beginRender(wikitext): Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
* wikitext - wikitext to render; most things should work, including subst: and ~~~~
*
* closePreview(): Hides the preview box and clears it.
*
* The suggested implementation pattern (in Morebits.simpleWindow + Morebits.quickForm situations) is to
* construct a Morebits.wiki.preview object after rendering a Morebits.quickForm, and bind the object
* to an arbitrary property of the form (e.g. |previewer|). For an example, see
* twinklewarn.js.
*/
Morebits.wiki.preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass("morebits-previewbox").hide();
this.beginRender = function(wikitext) {
$(previewbox).show();
var statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.status.init(statusspan);
var query = {
action: 'parse',
prop: 'text',
pst: 'true', // PST = pre-save transform; this makes substitution work properly
text: wikitext,
title: mw.config.get('wgPageName')
};
var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview"));
renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
var xml = apiobj.getXML();
var html = $(xml).find('text').text();
if (!html) {
apiobj.statelem.error("failed to retrieve preview, or template was blanked");
return;
}
previewbox.innerHTML = html;
};
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/**
* **************** Morebits.wikitext ****************
* Wikitext manipulation
*/
Morebits.wikitext = {};
Morebits.wikitext.template = {
parse: function( text, start ) {
var count = -1;
var level = -1;
var equals = -1;
var current = '';
var result = {
name: '',
parameters: {}
};
var key, value;
for( var i = start; i < text.length; ++i ) {
var test3 = text.substr( i, 3 );
if( test3 === '{{{' ) {
current += '{{{';
i += 2;
++level;
continue;
}
if( test3 === '}}}' ) {
current += '}}}';
i += 2;
--level;
continue;
}
var test2 = text.substr( i, 2 );
if( test2 === '{{' || test2 === '[[' ) {
current += test2;
++i;
++level;
continue;
}
if( test2 === '[[' ) {
current += test2;
++i;
--level;
continue;
}
if( test2 === '}}' ) {
current += test2;
++i;
--level;
if( level <= 0 ) {
if( count === -1 ) {
result.name = current.substring(2).trim();
++count;
} else {
if( equals !== -1 ) {
key = current.substring( 0, equals ).trim();
value = current.substring( equals ).trim();
result.parameters[key] = value;
equals = -1;
} else {
result.parameters[count] = current;
++count;
}
}
break;
}
continue;
}
if( text.charAt(i) === '|' && level <= 0 ) {
if( count === -1 ) {
result.name = current.substring(2).trim();
++count;
} else {
if( equals !== -1 ) {
key = current.substring( 0, equals ).trim();
value = current.substring( equals + 1 ).trim();
result.parameters[key] = value;
equals = -1;
} else {
result.parameters[count] = current;
++count;
}
}
current = '';
} else if( equals === -1 && text.charAt(i) === '=' && level <= 0 ) {
equals = current.length;
current += text.charAt(i);
} else {
current += text.charAt(i);
}
}
return result;
}
};
Morebits.wikitext.page = function mediawikiPage( text ) {
this.text = text;
};
Morebits.wikitext.page.prototype = {
text: '',
removeLink: function( link_target ) {
var first_char = link_target.substr( 0, 1 );
var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true );
var link_simple_re = new RegExp( "\\[\\[:?(" + link_re_string + ")\\]\\]", 'g' );
var link_named_re = new RegExp( "\\[\\[:?" + link_re_string + "\\|(.+?)\\]\\]", 'g' );
this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" );
},
commentOutImage: function( image, reason ) {
var unbinder = new Morebits.unbinder( this.text );
unbinder.unbind( '<!--', '-->' );
reason = reason ? (reason + ': ') : '';
var first_char = image.substr( 0, 1 );
var image_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( image.substr( 1 ), true );
/*
* Check for normal image links, i.e. [[Image:Foobar.png|...]]
* Will eat the whole link
*/
var links_re = new RegExp( "\\[\\[(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string );
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( unbinder.content, '[[', ']]' ));
for( var i = 0; i < allLinks.length; ++i ) {
if( links_re.test( allLinks[i] ) ) {
var replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace( allLinks[i], replacement, 'g' );
}
}
// unbind the newly created comments
unbinder.unbind( '<!--', '-->' );
/*
* Check for gallery images, i.e. instances that must start on a new line, eventually preceded with some space, and must include Image: prefix
* Will eat the whole line.
*/
var gallery_image_re = new RegExp( "(^\\s*(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string + ".*?$)", 'mg' );
unbinder.content.replace( gallery_image_re, "<!-- " + reason + "$1 -->" );
// unbind the newly created comments
unbinder.unbind( '<!--', '-->' );
/*
* Check free image usages, for example as template arguments, might have the Image: prefix excluded, but must be preceeded by an |
* Will only eat the image name and the preceeding bar and an eventual named parameter
*/
var free_image_re = new RegExp( "(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile):\\s*)?" + image_re_string + ")", 'mg' );
unbinder.content.replace( free_image_re, "<!-- " + reason + "$1 -->" );
// Rebind the content now, we are done!
this.text = unbinder.rebind();
},
addToImageComment: function( image, data ) {
var first_char = image.substr( 0, 1 );
var first_char_regex = RegExp.escape( first_char, true );
if( first_char.toUpperCase() !== first_char.toLowerCase() ) {
first_char_regex = '[' + RegExp.escape( first_char.toUpperCase(), true ) + RegExp.escape( first_char.toLowerCase(), true ) + ']';
}
var image_re_string = "(?:[Ii]mage|[Ff]ile):\\s*" + first_char_regex + RegExp.escape( image.substr( 1 ), true );
var links_re = new RegExp( "\\[\\[" + image_re_string );
var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '[[', ']]' ));
for( var i = 0; i < allLinks.length; ++i ) {
if( links_re.test( allLinks[i] ) ) {
var replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace( /\]\]$/, '|' + data + ']]' );
this.text = this.text.replace( allLinks[i], replacement, 'g' );
}
}
var gallery_re = new RegExp( "^(\\s*" + image_re_string + '.*?)\\|?(.*?)$', 'mg' );
var newtext = "$1|$2 " + data;
this.text = this.text.replace( gallery_re, newtext );
},
removeTemplate: function( template ) {
var first_char = template.substr( 0, 1 );
var template_re_string = "(?:[Tt]emplate:)?\\s*[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( template.substr( 1 ), true );
var links_re = new RegExp( "\\{\\{" + template_re_string );
var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '{{', '}}', [ '{{{', '}}}' ] ));
for( var i = 0; i < allTemplates.length; ++i ) {
if( links_re.test( allTemplates[i] ) ) {
this.text = this.text.replace( allTemplates[i], '', 'g' );
}
}
},
getText: function() {
return this.text;
}
};
/**
* **************** Morebits.queryString ****************
* Maps the querystring to an object
*
* Functions:
*
* Morebits.queryString.exists(key)
* returns true if the particular key is set
* Morebits.queryString.get(key)
* returns the value associated to the key
* Morebits.queryString.equals(key, value)
* returns true if the value associated with given key equals given value
* Morebits.queryString.toString()
* returns the query string as a string
* Morebits.queryString.create( hash )
* creates an querystring and encodes strings via encodeURIComponent and joins arrays with |
*
* In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object.
*
* Example:
*
* var value = Morebits.queryString.get('key');
* var obj = new Morebits.queryString('foo=bar&baz=quux');
* value = obj.get('foo');
*/
Morebits.queryString = function QueryString(qString) {
this.string = qString;
this.params = {};
if( !qString.length ) {
return;
}
qString.replace(/\+/, ' ');
var args = qString.split('&');
for( var i = 0; i < args.length; ++i ) {
var pair = args[i].split( '=' );
var key = decodeURIComponent( pair[0] ), value = key;
if( pair.length === 2 ) {
value = decodeURIComponent( pair[1] );
}
this.params[key] = value;
}
};
Morebits.queryString.staticstr = null;
Morebits.queryString.staticInit = function() {
if( !Morebits.queryString.staticstr ) {
Morebits.queryString.staticstr = new Morebits.queryString(location.search.substring(1));
}
};
Morebits.queryString.get = function(key) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.get(key);
};
Morebits.queryString.prototype.get = function(key) {
return this.params[key] ? this.params[key] : null;
};
Morebits.queryString.exists = function(key) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.exists(key);
};
Morebits.queryString.prototype.exists = function(key) {
return this.params[key] ? true : false;
};
Morebits.queryString.equals = function(key, value) {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.equals(key, value);
};
Morebits.queryString.prototype.equals = function(key, value) {
return this.params[key] === value ? true : false;
};
Morebits.queryString.toString = function() {
Morebits.queryString.staticInit();
return Morebits.queryString.staticstr.toString();
};
Morebits.queryString.prototype.toString = function() {
return this.string ? this.string : null;
};
Morebits.queryString.create = function( arr ) {
var resarr = [];
var editToken; // KLUGE: this should always be the last item in the query string (bug TW-B-0013)
for( var i in arr ) {
if( arr[i] === undefined ) {
continue;
}
var res;
if( $.isArray( arr[i] ) ){
var v = [];
for(var j = 0; j < arr[i].length; ++j ) {
v[j] = encodeURIComponent( arr[i][j] );
}
res = v.join('|');
} else {
res = encodeURIComponent( arr[i] );
}
if( i === 'token' ) {
editToken = res;
} else {
resarr.push( encodeURIComponent( i ) + '=' + res );
}
}
if( editToken !== undefined ) {
resarr.push( 'token=' + editToken );
}
return resarr.join('&');
};
Morebits.queryString.prototype.create = Morebits.queryString.create;
/**
* **************** Morebits.status ****************
*/
Morebits.status = function Status( text, stat, type ) {
this.textRaw = text;
this.text = this.codify(text);
this.type = type || 'status';
this.generate();
if( stat ) {
this.update( stat, type );
}
};
Morebits.status.init = function( root ) {
if( !( root instanceof Element ) ) {
throw new Error( 'object not an instance of Element' );
}
while( root.hasChildNodes() ) {
root.removeChild( root.firstChild );
}
Morebits.status.root = root;
Morebits.status.errorEvent = null;
};
Morebits.status.root = null;
Morebits.status.onError = function( handler ) {
if ( $.isFunction( handler ) ) {
Morebits.status.errorEvent = handler;
} else {
throw "Morebits.status.onError: handler is not a function";
}
};
Morebits.status.prototype = {
stat: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
link: function() {
if( ! this.linked && Morebits.status.root ) {
Morebits.status.root.appendChild( this.node );
this.linked = true;
}
},
unlink: function() {
if( this.linked ) {
Morebits.status.root.removeChild( this.node );
this.linked = false;
}
},
codify: function( obj ) {
if ( ! $.isArray( obj ) ) {
obj = [ obj ];
}
var result;
result = document.createDocumentFragment();
for( var i = 0; i < obj.length; ++i ) {
if( typeof obj[i] === 'string' ) {
result.appendChild( document.createTextNode( obj[i] ) );
} else if( obj[i] instanceof Element ) {
result.appendChild( obj[i] );
} // Else cosmic radiation made something shit
}
return result;
},
update: function( status, type ) {
this.stat = this.codify( status );
if( type ) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.status.errorEvent) {
Morebits.status.errorEvent();
}
// also log error messages in the browser console
if (console && console.error) {
console.error(this.textRaw + ": " + status);
}
}
}
this.render();
},
generate: function() {
this.node = document.createElement( 'div' );
this.node.appendChild( document.createElement('span') ).appendChild( this.text );
this.node.appendChild( document.createElement('span') ).appendChild( document.createTextNode( ': ' ) );
this.target = this.node.appendChild( document.createElement( 'span' ) );
this.target.appendChild( document.createTextNode( '' ) ); // dummy node
},
render: function() {
this.node.className = 'tw_status_' + this.type;
while( this.target.hasChildNodes() ) {
this.target.removeChild( this.target.firstChild );
}
this.target.appendChild( this.stat );
this.link();
},
status: function( status ) {
this.update( status, 'status');
},
info: function( status ) {
this.update( status, 'info');
},
warn: function( status ) {
this.update( status, 'warn');
},
error: function( status ) {
this.update( status, 'error');
}
};
Morebits.status.info = function( text, status ) {
return new Morebits.status( text, status, 'info' );
};
Morebits.status.warn = function( text, status ) {
return new Morebits.status( text, status, 'warn' );
};
Morebits.status.error = function( text, status ) {
return new Morebits.status( text, status, 'error' );
};
/**
* **************** Morebits.htmlNode() ****************
* Simple helper function to create a simple node
*/
Morebits.htmlNode = function ( type, content, color ) {
var node = document.createElement( type );
if( color ) {
node.style.color = color;
}
node.appendChild( document.createTextNode( content ) );
return node;
}
/**
* **************** Morebits.simpleWindow ****************
* A simple draggable window
* now a wrapper for jQuery UI's dialog feature
*/
// The height passed in here is the maximum allowable height for the content area.
Morebits.simpleWindow = function SimpleWindow( width, height ) {
var content = document.createElement( 'div' );
this.content = content;
content.className = 'morebits-dialog-content';
this.height = height;
$(this.content).dialog({
autoOpen: false,
buttons: { "Placeholder button": function() {} },
dialogClass: 'morebits-dialog',
width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)),
// give jQuery the given height value (which represents the anticipated height of the dialog) here, so
// it can position the dialog appropriately
// the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared
// to that of the old SimpleWindow
height: height + 20,
close: function(event, ui) {
// dialogs and their content can be destroyed once closed
$(event.target).dialog("destroy").remove();
},
resize: function(event, ui) {
this.style.maxHeight = "";
}
});
var $widget = $(this.content).dialog("widget");
// add background gradient to titlebar
var $titlebar = $widget.find(".ui-dialog-titlebar");
var oldstyle = $titlebar.attr("style");
$titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;');
// delete the placeholder button (it's only there so the buttonpane gets created)
$widget.find("button").each(function(key, value) {
value.parentNode.removeChild(value);
});
// add container for the buttons we add, and the footer links (if any)
var buttonspan = document.createElement("span");
buttonspan.className = "morebits-dialog-buttons";
var linksspan = document.createElement("span");
linksspan.className = "morebits-dialog-footerlinks";
$widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan);
};
Morebits.simpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
// Focuses the dialog. This might work, or on the contrary, it might not.
focus: function(event) {
$(this.content).dialog("moveToTop");
return this;
},
// Closes the dialog. If this is set as an event handler, it will stop the event from doing anything more.
close: function(event) {
if (event) {
event.preventDefault();
}
$(this.content).dialog("close");
return this;
},
// Shows the dialog. Calling display() on a dialog that has previously been closed might work, but it is not guaranteed.
display: function() {
if (this.scriptName) {
var $widget = $(this.content).dialog("widget");
$widget.find(".morebits-dialog-scriptname").remove();
var scriptnamespan = document.createElement("span");
scriptnamespan.className = "morebits-dialog-scriptname";
scriptnamespan.textContent = this.scriptName + " \u00B7 "; // U+00B7 MIDDLE DOT = ·
$widget.find(".ui-dialog-title").prepend(scriptnamespan);
}
var dialog = $(this.content).dialog("open");
if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) { // tie in with NAVPOP
dialog.parent()[0].ranSetupTooltipsAlready = false;
setupTooltips(dialog.parent()[0]);
}
this.setHeight( this.height ); // init height algorithm
return this;
},
// Sets the dialog title.
setTitle: function( title ) {
$(this.content).dialog("option", "title", title);
return this;
},
// Sets the script name, appearing as a prefix to the title to help users determine which
// user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
setScriptName: function( name ) {
this.scriptName = name;
return this;
},
// Sets the dialog width.
setWidth: function( width ) {
$(this.content).dialog("option", "width", width);
return this;
},
// Sets the dialog's maximum height. The dialog will auto-size to fit its contents,
// but the content area will grow no larger than the height given here.
setHeight: function( height ) {
this.height = height;
// from display time onwards, let the browser determine the optimum height, and instead limit the height at the given value
// note that the given height will exclude the approx. 20px that the jQuery UI chrome has in height in addition to the height
// of an equivalent "classic" Morebits.simpleWindow
if (parseInt(getComputedStyle($(this.content).dialog("widget")[0], null).height, 10) > window.innerHeight) {
$(this.content).dialog("option", "height", window.innerHeight - 2).dialog("option", "position", "top");
} else {
$(this.content).dialog("option", "height", "auto");
}
$(this.content).dialog("widget").find(".morebits-dialog-content")[0].style.maxHeight = parseInt(this.height - 30, 10) + "px";
return this;
},
// Sets the content of the dialog to the given element node, usually from rendering a Morebits.quickForm.
// Re-enumerates the footer buttons, but leaves the footer links as they are.
// Be sure to call this at least once before the dialog is displayed...
setContent: function( content ) {
this.purgeContent();
this.addContent( content );
return this;
},
addContent: function( content ) {
this.content.appendChild( content );
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
var thisproxy = this;
$(this.content).find('input[type="submit"], button[type="submit"]').each(function(key, value) {
value.style.display = "none";
var button = document.createElement("button");
button.textContent = (value.hasAttribute("value") ? value.getAttribute("value") : (value.textContent ? value.textContent : "Submit Query"));
// here is an instance of cheap coding, probably a memory-usage hit in using a closure here
button.addEventListener("click", function() { value.click(); }, false);
thisproxy.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
if (this.buttons.length > 0) {
$(this.content).dialog("widget").find(".morebits-dialog-buttons").empty().append(this.buttons)[0].removeAttribute("data-empty");
} else {
$(this.content).dialog("widget").find(".morebits-dialog-buttons")[0].setAttribute("data-empty", "data-empty"); // used by CSS
}
return this;
},
purgeContent: function( content ) {
this.buttons = [];
// delete all buttons in the buttonpane
$(this.content).dialog("widget").find(".morebits-dialog-buttons").empty();
while( this.content.hasChildNodes() ) {
this.content.removeChild( this.content.firstChild );
}
return this;
},
// Adds a link in the bottom-right corner of the dialog.
// This can be used to provide help or policy links.
// For example, Twinkle's CSD module adds a link to the CSD policy page,
// as well as a link to Twinkle's documentation.
addFooterLink: function( text, wikiPage ) {
var $footerlinks = $(this.content).dialog("widget").find(".morebits-dialog-footerlinks");
if (this.hasFooterLinks) {
var bullet = document.createElement("span");
bullet.textContent = " \u2022 "; // U+2022 BULLET
$footerlinks.append(bullet);
}
var link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(wikiPage) );
link.setAttribute("title", wikiPage);
link.setAttribute("target", "_blank");
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
setModality: function( modal ) {
$(this.content).dialog("option", "modal", modal);
return this;
}
};
// Enables or disables all footer buttons on all Morebits.simpleWindows in the current page.
// This should be called with |false| when the button(s) become irrelevant (e.g. just before Morebits.status.init is called).
// This is not an instance method so that consumers don't have to keep a reference to the original
// Morebits.simpleWindow object sitting around somewhere. Anyway, most of the time there will only be one
// Morebits.simpleWindow open, so this shouldn't matter.
Morebits.simpleWindow.setButtonsEnabled = function( enabled ) {
$(".morebits-dialog-buttons button").attr("disabled", !enabled);
};
// Twinkle blacklist was removed per consensus at http://en.wikipedia.org/wiki/Wikipedia:Administrators%27_noticeboard/Archive221#New_Twinkle_blacklist_proposal
} ( window, document, jQuery )); // End wrap with anonymous function
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if ( typeof arguments === "undefined" ) { // typeof is here for a reason...
window.SimpleWindow = Morebits.simpleWindow;
window.QuickForm = Morebits.quickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.status;
window.QueryString = Morebits.queryString;
}
// </nowiki>
// [[Category:Scripts]]
// [[Category:Twinkle]]
be7ce576a8bd7447153c70167bdc568d1cc37459
MediaWiki:Gadget-Twinkle.js
8
83
159
158
2023-08-28T02:15:25Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
javascript
text/javascript
/**
Twinkle.js [[Category:Twinkle]]
Forked from simplewiki's version of Twinkle and de-Wikipedia-fied by Naleksuh
Currently in beta and still has some reseblences to Wikipedia. Will be more fine-tuned over time
*/
//<nowiki>
mw.loader.load("https://dev.miraheze.org/w/index.php?title=MediaWiki:Jquerymigrate-3.3.2.js&action=raw&ctype=text/javascript");
( function ( window, document, $, undefined ) { // Wrap with anonymous function
var Twinkle = {};
window.Twinkle = Twinkle; // allow global access
// for use by custom modules (normally empty)
Twinkle.initCallbacks = [];
Twinkle.addInitCallback = function twinkleAddInitCallback( func ) {
Twinkle.initCallbacks.push( func );
};
Twinkle.defaultConfig = {};
/**
* Twinkle.defaultConfig.twinkle and Twinkle.defaultConfig.friendly
*
* This holds the default set of preferences used by Twinkle. (The |friendly| object holds preferences stored in the FriendlyConfig object.)
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig.twinkle = {
// General
summaryAd: " ([[mh:dev:Twinkle|TW]])",
deletionSummaryAd: " ([[mh:dev:Twinkle|TW]])",
protectionSummaryAd: " ([[mh:dev:Twinkle|TW]])",
userTalkPageMode: "window",
dialogLargeFont: false,
// Fluff (revert and rollback)
openTalkPage: [ "agf", "norm", "vand" ],
openTalkPageOnAutoRevert: false,
markRevertedPagesAsMinor: [ "vand" ],
watchRevertedPages: [ "agf", "norm", "vand", "torev" ],
offerReasonOnNormalRevert: true,
confirmOnFluff: false,
showRollbackLinks: [ "diff", "others" ],
// CSD
speedySelectionStyle: "buttonClick",
speedyPromptOnG7: false,
watchSpeedyPages: [ "g3", "g5", "g10", "g11", "g12" ],
markSpeedyPagesAsPatrolled: true,
// these next two should probably be identical by default
notifyUserOnSpeedyDeletionNomination: [ ],
welcomeUserOnSpeedyDeletionNotification: [ ],
promptForSpeedyDeletionSummary: [ "db" ],
openUserTalkPageOnSpeedyDelete: [ "db"],
deleteTalkPageOnDelete: false,
deleteSysopDefaultToTag: false,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: "Deletion request log",
noLogOnSpeedyNomination: [ "u1" ],
// Unlink
unlinkNamespaces: [ "0" ],
// Warn
defaultWarningGroup: "1",
showSharedIPNotice: true,
watchWarnings: true,
blankTalkpageOnIndefBlock: false,
// XfD
xfdWatchDiscussion: "default",
xfdWatchList: "no",
xfdWatchPage: "default",
xfdWatchUser: "default",
// Hidden preferences
revertMaxRevisions: 50,
batchdeleteChunks: 50,
batchDeleteMinCutOff: 5,
batchMax: 5000,
batchProtectChunks: 50,
batchProtectMinCutOff: 5,
batchundeleteChunks: 50,
batchUndeleteMinCutOff: 5
};
// now some skin dependent config.
if ( mw.config.get( "skin" ) === "vector" || mw.config.get("skin") === "vector-2022") {
Twinkle.defaultConfig.twinkle.portletArea = "right-navigation";
Twinkle.defaultConfig.twinkle.portletId = "p-twinkle";
Twinkle.defaultConfig.twinkle.portletName = "TW";
Twinkle.defaultConfig.twinkle.portletType = "menu";
Twinkle.defaultConfig.twinkle.portletNext = "p-search";
} else {
Twinkle.defaultConfig.twinkle.portletArea = null;
Twinkle.defaultConfig.twinkle.portletId = "p-cactions";
Twinkle.defaultConfig.twinkle.portletName = null;
Twinkle.defaultConfig.twinkle.portletType = null;
Twinkle.defaultConfig.twinkle.portletNext = null;
}
Twinkle.defaultConfig.friendly = {
// Tag
groupByDefault: true,
watchTaggedPages: true,
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: true,
tagArticleSortOrder: "cat",
customTagList: [],
// Stub
watchStubbedPages: true,
markStubbedPagesAsMinor: false,
markStubbedPagesAsPatrolled: true,
stubArticleSortOrder: "cat",
// Welcome
topWelcomes: false,
watchWelcomes: true,
welcomeHeading: "Welcome",
insertHeadings: true,
insertUsername: true,
insertSignature: true, // sign welcome templates, where appropriate
quickWelcomeMode: "norm",
quickWelcomeTemplate: "welcome",
customWelcomeList: [],
// Talkback
markTalkbackAsMinor: true,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: "Talkback",
adminNoticeHeading: "Notice",
mailHeading: "You've got mail!",
// Shared
markSharedIPAsMinor: true
};
Twinkle.getPref = function twinkleGetPref( name ) {
var result;
if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.twinkle === "object" ) {
// look in Twinkle.prefs (twinkleoptions.js)
result = Twinkle.prefs.twinkle[name];
} else if ( typeof window.TwinkleConfig === "object" ) {
// look in TwinkleConfig
result = window.TwinkleConfig[name];
}
if ( result === undefined ) {
return Twinkle.defaultConfig.twinkle[name];
}
return result;
};
Twinkle.getFriendlyPref = function twinkleGetFriendlyPref(name) {
var result;
if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.friendly === "object" ) {
// look in Twinkle.prefs (twinkleoptions.js)
result = Twinkle.prefs.friendly[ name ];
} else if ( typeof window.FriendlyConfig === "object" ) {
// look in FriendlyConfig
result = window.FriendlyConfig[ name ];
}
if ( result === undefined ) {
return Twinkle.defaultConfig.friendly[ name ];
}
return result;
};
/**
* **************** twAddPortlet() ****************
*
* Adds a portlet menu to one of the navigation areas on the page.
* This is necessarily quite a hack since skins, navigation areas, and
* portlet menu types all work slightly different.
*
* Available navigation areas depend on the skin used.
* Vector:
* For each option, the outer div class contains "vector-menu", the inner div class is "vector-menu-content", and the ul is "vector-menu-content-list"
* "mw-panel", outer div class contains "vector-menu-portal". Existing portlets/elements: "p-logo", "p-navigation", "p-interaction", "p-tb", "p-coll-print_export"
* "left-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-namespaces", "p-variants" (menu)
* "right-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-views", "p-cactions" (menu), "p-search"
* Special layout of p-personal portlet (part of "head") through specialized styles.
* Monobook:
* "column-one", outer div class "portlet", inner div class "pBody". Existing portlets: "p-cactions", "p-personal", "p-logo", "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export"
* Special layout of p-cactions and p-personal through specialized styles.
* Modern:
* "mw_contentwrapper" (top nav), outer div class "portlet", inner div class "pBody". Existing portlets or elements: "p-cactions", "mw_content"
* "mw_portlets" (sidebar), outer div class "portlet", inner div class "pBody". Existing portlets: "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export"
*
* @param String navigation -- id of the target navigation area (skin dependant, on vector either of "left-navigation", "right-navigation", or "mw-panel")
* @param String id -- id of the portlet menu to create, preferably start with "p-".
* @param String text -- name of the portlet menu to create. Visibility depends on the class used.
* @param String type -- type of portlet. Currently only used for the vector non-sidebar portlets, pass "menu" to make this portlet a drop down menu.
* @param Node nextnodeid -- the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end.
*
* @return Node -- the DOM node of the new item (a DIV element) or null
*/
function twAddPortlet( navigation, id, text, type, nextnodeid )
{
//sanity checks, and get required DOM nodes
var root = document.getElementById( navigation );
if ( !root ) {
return null;
}
var item = document.getElementById( id );
if ( item ) {
if ( item.parentNode && item.parentNode === root ) {
return item;
}
return null;
}
var nextnode;
if ( nextnodeid ) {
nextnode = document.getElementById(nextnodeid);
}
if ((mw.config.get('skin') !== 'vector' && mw.config.get('skin') !== 'vector-2022') || (navigation !== 'left-navigation' && navigation !== 'right-navigation')) {
type = null; // menu supported only in vector's #left-navigation & #right-navigation
}
var outerDivClass;
var innerDivClass;
switch (mw.config.get('skin'))
{
case "vector":
case 'vector-2022':
if ( navigation !== "portal" && navigation !== "left-navigation" && navigation !== "right-navigation" ) {
navigation = "mw-panel";
}
outerDivClass = 'vector-menu vector-menu-' + (navigation === 'mw-panel' ? 'portal' : type === 'menu' ? 'dropdown vector-menu-dropdown-noicon' : 'tabs');
innerDivClass = 'vector-menu-content';
break;
case "modern":
if ( navigation !== "mw_portlets" && navigation !== "mw_contentwrapper" ) {
navigation = "mw_portlets";
}
outerDivClass = "portlet";
innerDivClass = "pBody";
break;
default:
navigation = "column-one";
outerDivClass = "portlet";
innerDivClass = "pBody";
break;
}
// Build the DOM elements.
var outerDiv = document.createElement('nav');
outerDiv.setAttribute('aria-labelledby', id + '-label');
// Vector getting vector-menu-empty FIXME TODO
outerDiv.className = outerDivClass + ' emptyPortlet';
outerDiv.id = id;
if (nextnode && nextnode.parentNode === root) {
root.insertBefore(outerDiv, nextnode);
} else {
root.appendChild(outerDiv);
}
var h3 = document.createElement('h3');
h3.id = id + '-label';
var ul = document.createElement('ul');
if (mw.config.get( "skin" ) === 'vector' || mw.config.get("skin") === 'vector-2022') {
h3.className = "vector-menu-heading";
// add invisible checkbox to keep menu open when clicked
// similar to the p-cactions ("More") menu
if (outerDivClass.indexOf('vector-menu-dropdown') !== -1) {
var chkbox = document.createElement('input');
chkbox.className = 'vectorMenuCheckbox vector-menu-checkbox'; // remove vectorMenuCheckbox after 1.35-wmf.37 goes live
chkbox.setAttribute('type', 'checkbox');
chkbox.setAttribute('aria-labelledby', id + '-label');
outerDiv.appendChild(chkbox);
var span = document.createElement('span');
span.appendChild(document.createTextNode(text));
h3.appendChild(span);
var a = document.createElement('a');
a.href = '#';
$(a).click(function(e) {
e.preventDefault();
});
h3.appendChild(a);
}
outerDiv.appendChild(h3);
ul.className = 'menu vector-menu-content-list'; // remove menu after 1.35-wmf.37 goes live
} else {
h3.appendChild(document.createTextNode(text));
outerDiv.appendChild(h3);
}
if (innerDivClass) {
var innerDiv = document.createElement('div');
innerDiv.className = innerDivClass;
innerDiv.appendChild(ul);
outerDiv.appendChild(innerDiv);
} else {
outerDiv.appendChild(ul);
}
return outerDiv;
}
/**
* **************** twAddPortletLink() ****************
* Builds a portlet menu if it doesn't exist yet, and add the portlet link.
* @param task: Either a URL for the portlet link or a function to execute.
*/
function twAddPortletLink( task, text, id, tooltip )
{
if ( Twinkle.getPref("portletArea") !== null ) {
twAddPortlet( Twinkle.getPref( "portletArea" ), Twinkle.getPref( "portletId" ), Twinkle.getPref( "portletName" ), Twinkle.getPref( "portletType" ), Twinkle.getPref( "portletNext" ));
}
var link = mw.util.addPortletLink( Twinkle.getPref( "portletId" ), typeof task === "string" ? task : "#", text, id, tooltip );
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
if ( $.isFunction( task ) ) {
$( link ).click(function ( ev ) {
task();
ev.preventDefault();
});
}
if ($.collapsibleTabs) {
$.collapsibleTabs.handleResize();
}
return link;
}
// Check if account is experienced enough to use Twinkle
var twinkleUserAuthorized = Morebits.userIsInGroup( "autoconfirmed" ) || Morebits.userIsInGroup( "confirmed" ) || Morebits.userIsInGroup( "sysop" );
/*
****************************************
*** friendlyshared.js: Shared IP tagging module
****************************************
* Mode of invocation: Tab ("Shared")
* Active on: Existing IP user talk pages
* Config directives in: FriendlyConfig
*/
Twinkle.shared = function friendlyshared() {
if( mw.config.get('wgNamespaceNumber') === 3 && Morebits.isIPAddress(mw.config.get('wgTitle')) ) {
var username = mw.config.get('wgTitle').split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes
twAddPortletLink( function(){ Twinkle.shared.callback(username); }, "Shared IP", "friendly-shared", "Shared IP tagging" );
}
};
Twinkle.shared.callback = function friendlysharedCallback( uid ) {
var Window = new Morebits.simpleWindow( 600, 400 );
Window.setTitle( "Shared IP address tagging" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#shared" );
var form = new Morebits.quickForm( Twinkle.shared.callback.evaluate );
var div = form.append( { type: 'div', id: 'sharedip-templatelist' } );
div.append( { type: 'header', label: 'Shared IP address templates' } );
div.append( { type: 'radio', name: 'shared', list: Twinkle.shared.standardList,
event: function( e ) {
Twinkle.shared.callback.change_shared( e );
e.stopPropagation();
}
} );
var org = form.append( { type:'field', label:'Fill in other details (optional) and click \"Submit\"' } );
org.append( {
type: 'input',
name: 'organization',
label: 'IP address owner/operator',
disabled: true,
tooltip: 'You can optionally enter the name of the organization that owns/operates the IP address. You can use wikimarkup if necessary.'
}
);
org.append( {
type: 'input',
name: 'host',
label: 'Host name (optional)',
disabled: true,
tooltip: 'The host name (for example, proxy.example.com) can be optionally entered here and will be linked by the template.'
}
);
org.append( {
type: 'input',
name: 'contact',
label: 'Contact information (only if requested)',
disabled: true,
tooltip: 'You can optionally enter some contact details for the organization. Use this parameter only if the organization has specifically requested that it be added. You can use wikimarkup if necessary.'
}
);
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
$(result).find('div#sharedip-templatelist').addClass('quickform-scrollbox');
};
Twinkle.shared.standardList = [
{
label: '{{SharedIP}}: standard shared IP address template',
value: 'Shared IP',
tooltip: 'IP user talk page template that shows helpful information to IP users and those wishing to warn, block or ban them'
},
{
label: '{{SchoolIP}}: shared IP address template modified for educational institutions',
value: 'SchoolIP'
},
{
label: '{{SharedIPCORP}}: shared IP address template modified for businesses',
value: 'SharedIPCORP'
},
{
label: '{{ISP}}: shared IP address template modified for ISP organizations (specifically proxies)',
value: 'ISP'
}
];
Twinkle.shared.callback.change_shared = function friendlysharedCallbackChangeShared(e) {
if( e.target.value === 'Shared IP edu' ) {
e.target.form.contact.disabled = false;
} else {
e.target.form.contact.disabled = true;
}
e.target.form.organization.disabled=false;
e.target.form.host.disabled=false;
};
Twinkle.shared.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters();
var pageText = pageobj.getPageText();
var found = false;
var text = '{{';
for( var i=0; i < Twinkle.shared.standardList.length; i++ ) {
var tagRe = new RegExp( '(\\{\\{' + Twinkle.shared.standardList[i].value + '(\\||\\}\\}))', 'im' );
if( tagRe.exec( pageText ) ) {
Morebits.status.warn( 'Info', 'Found {{' + Twinkle.shared.standardList[i].value + '}} on the user\'s talk page already...aborting' );
found = true;
}
}
if( found ) {
return;
}
Morebits.status.info( 'Info', 'Will add the shared IP address template to the top of the user\'s talk page.' );
text += params.value + '|' + params.organization;
if( params.value === 'shared IP edu' && params.contact !== '') {
text += '|' + params.contact;
}
if( params.host !== '' ) {
text += '|host=' + params.host;
}
text += '}}\n\n';
var summaryText = 'Added {{[[Template:' + params.value + '|' + params.value + ']]}} template.';
pageobj.setPageText(text + pageText);
pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markSharedIPAsMinor'));
pageobj.setCreateOption('recreate');
pageobj.save();
}
};
Twinkle.shared.callback.evaluate = function friendlysharedCallbackEvaluate(e) {
var shared = e.target.getChecked( 'shared' );
if( !shared || shared.length <= 0 ) {
alert( 'You must select a shared IP address template to use!' );
return;
}
var value = shared[0];
if( e.target.organization.value === '') {
alert( 'You must input an organization for the {{' + value + '}} template!' );
return;
}
var params = {
value: value,
organization: e.target.organization.value,
host: e.target.host.value,
contact: e.target.contact.value
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.shared.callbacks.main);
};
/*
****************************************
*** friendlytag.js: Tag module
****************************************
* Mode of invocation: Tab ("Tag")
* Active on: Existing articles; file pages with a corresponding file
* which is local (not on Commons); existing subpages of
* {Wikipedia|Wikipedia talk}:Articles for creation;
* all redirects
* Config directives in: FriendlyConfig
*/
Twinkle.tag = function friendlytag() {
// redirect tagging
if( Morebits.wiki.isPageRedirect() ) {
Twinkle.tag.mode = 'redirect';
//twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Tag redirect" );
}
// file tagging
else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) {
Twinkle.tag.mode = 'file';
}
// article/draft article tagging
else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) {
Twinkle.tag.mode = 'article';
//twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Add maintenance tags to article" );
}
};
Twinkle.tag.callback = function friendlytagCallback( uid ) {
var Window = new Morebits.simpleWindow( 630, (Twinkle.tag.mode === "article") ? 450 : 400 );
Window.setScriptName( "Twinkle" );
// anyone got a good policy/guideline/info page/instructional page link??
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#tag" );
var form = new Morebits.quickForm( Twinkle.tag.callback.evaluate );
switch( Twinkle.tag.mode ) {
case 'article':
Window.setTitle( "Article maintenance tagging" );
form.append( {
type: 'checkbox',
list: [
{
label: 'Group inside {{multiple issues}} if possible',
value: 'group',
name: 'group',
tooltip: 'If applying three or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.',
checked: Twinkle.getFriendlyPref('groupByDefault')
}
]
}
);
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (mh:dev:Twinkle/Preferences).',
event: Twinkle.tag.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'alpha' }
]
});
form.append( { type: 'div', id: 'tagWorkArea' } );
if( Twinkle.getFriendlyPref('customTagList').length ) {
form.append( { type: 'header', label: 'Custom tags' } );
form.append( { type: 'checkbox', name: 'articleTags', list: Twinkle.getFriendlyPref('customTagList') } );
}
break;
case 'redirect':
Window.setTitle( "Redirect tagging" );
//Spelling, misspelling, tense and capitalization templates
form.append({ type: 'header', label:'All templates' });
form.append({ type: 'checkbox', name: 'redirectTags', list: Twinkle.tag.spellingList });
break;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
if (Twinkle.tag.mode === "article") {
// fake a change event on the sort dropdown, to initialize the tag list
var evt = document.createEvent("Event");
evt.initEvent("change", true, true);
result.sortorder.dispatchEvent(evt);
}
};
Twinkle.tag.checkedTags = [];
Twinkle.tag.updateSortOrder = function(e) {
var sortorder = e.target.value;
var $workarea = $(e.target.form).find("div#tagWorkArea");
Twinkle.tag.checkedTags = e.target.form.getChecked("articleTags");
if (!Twinkle.tag.checkedTags) {
Twinkle.tag.checkedTags = [];
}
// function to generate a checkbox, with appropriate subgroup if needed
var makeCheckbox = function(tag, description) {
var checkbox = { value: tag, label: "{{" + tag + "}}: " + description };
if (Twinkle.tag.checkedTags.indexOf(tag) !== -1) {
checkbox.checked = true;
}
if (tag === "notability") {
checkbox.subgroup = {
name: 'notability',
type: 'select',
list: [
{ label: "{{notability}}: article\'s subject may not meet the general notability guideline", value: "none" },
{ label: "{{notability|Academics}}: notability guideline for academics", value: "Academics" },
{ label: "{{notability|Biographies}}: notability guideline for biographies", value: "Biographies" },
{ label: "{{notability|Books}}: notability guideline for books", value: "Books" },
{ label: "{{notability|Companies}}: notability guidelines for companies and organizations", value: "Companies" },
{ label: "{{notability|Events}}: notability guideline for events", value: "Events" },
{ label: "{{notability|Films}}: notability guideline for films", value: "Films" },
{ label: "{{notability|Music}}: notability guideline for music", value: "Music" },
{ label: "{{notability|Neologisms}}: notability guideline for neologisms", value: "Neologisms" },
{ label: "{{notability|Numbers}}: notability guideline for numbers", value: "Numbers" },
{ label: "{{notability|Products}}: notability guideline for products and services", value: "Products" },
{ label: "{{notability|Sport}}: notability guideline for sports and athletics", value: "Sport" },
{ label: "{{notability|Web}}: notability guideline for web content", value: "Web" }
]
};
}
return checkbox;
};
// categorical sort order
if (sortorder === "cat") {
var div = new Morebits.quickForm.element({
type: "div",
id: "tagWorkArea"
});
// function to iterate through the tags and create a checkbox for each one
var doCategoryCheckboxes = function(subdiv, array) {
var checkboxes = [];
$.each(array, function(k, tag) {
var description = Twinkle.tag.article.tags[tag];
checkboxes.push(makeCheckbox(tag, description));
});
subdiv.append({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
};
var i = 0;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.tag.article.tagCategories, function(title, content) {
div.append({ type: "header", id: "tagHeader" + i, label: title });
var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ });
if ($.isArray(content)) {
doCategoryCheckboxes(subdiv, content);
} else {
$.each(content, function(subtitle, subcontent) {
subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] });
doCategoryCheckboxes(subdiv, subcontent);
});
}
});
var rendered = div.render();
$workarea.replaceWith(rendered);
var $rendered = $(rendered);
$rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' });
$rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' });
}
// alphabetical sort order
else {
var checkboxes = [];
$.each(Twinkle.tag.article.tags, function(tag, description) {
checkboxes.push(makeCheckbox(tag, description));
});
var tags = new Morebits.quickForm.element({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
$workarea.empty().append(tags.render());
}
};
// Tags for ARTICLES start here
Twinkle.tag.article = {};
// A list of all article tags, in alphabetical order
// To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below.
Twinkle.tag.article.tags = {
"advertisement": "article is written like an advertisement",
"autobiography": "article is an autobiography and may not be written neutrally",
"BLP sources": "BLP article needs more sources for verification",
"BLP unsourced": "BLP article has no sources at all",
"citation style": "article has unclear or inconsistent inline citations",
"cleanup": "article may require cleanup",
"COI": "article creator or major contributor may have a conflict of interest",
"complex": "the English used in this article or section may not be easy for everybody to understand",
"confusing": "article may be confusing or unclear",
"context": "article provides insufficient context",
"copyedit": "article needs copy editing for grammar, style, cohesion, tone, and/or spelling",
"dead end": "article has few or no links to other articles",
"disputed": "article has questionable factual accuracy",
"expert-subject": "article needs attention from an expert on the subject",
"external links": "article's external links may not follow content policies or guidelines",
"fansite": "article resembles a fansite",
"fiction": "article fails to distinguish between fact and fiction",
"globalise": "article may not represent a worldwide view of the subject",
"hoax": "article may be a complete hoax",
"in-universe": "article subject is fictional and needs rewriting from a non-fictional perspective",
"in use": "article is undergoing a major edit for a short while",
"intro-missing": "article has no lead section and one should be written",
"intro-rewrite": "article lead section needs to be rewritten",
"intro-tooshort": "article lead section is too short and should be expanded",
"jargon": "article uses technical words that not everybody will know",
"link rot": "article uses bare URLs for references, which are prone to link rot",
"merge": "article should be merged with another given article",
"metricate": "article exclusively uses non-SI units of measurement",
"more footnotes": "article has some references, but insufficient in-text citations",
"more sources": "article needs more sources for verification",
"no footnotes": "article has references, but no in-text citations",
"no sources": "article has no references at all",
"notability": "article's subject may not meet the notability guideline",
"NPOV": "article does not maintain a neutral point of view",
"one source": "article relies largely or entirely upon a single source",
"original research": "article has original research or unverified claims",
"orphan": "article is linked to from no other articles",
"plot": "plot summary in article is too long",
"primary sources": "article relies too heavily on first-hand sources, and needs third-party sources",
"prose": "article is in a list format that may be better presented using prose",
"redlinks": "article may have too many red links",
"restructure": "article may be in need of reorganization to comply with Wikipedia's layout guidelines",
"rough translation": "article is poorly translated and needs cleanup",
"sections": "article needs to be broken into sections",
"self-published": "article may contain improper references to self-published sources",
"tone": "tone of article is not appropriate",
"uncat": "article is uncategorized",
"under construction": "article is currently in the middle of an expansion or major revamping",
"unreliable sources": "article's references may not be reliable sources",
"update": "article needs additional up-to-date information added",
"very long": "article is too long",
"weasel": "article neutrality is compromised by the use of weasel words",
"wikify": "article needs to be wikified"
};
// A list of tags in order of category
// Tags should be in alphabetical order within the categories
// Add new categories with discretion - the list is long enough as is!
Twinkle.tag.article.tagCategories = {
"Cleanup and maintenance tags": {
"General maintenance tags": [
"cleanup",
"complex",
"copyedit",
"wikify"
],
"Potentially unwanted content": [
"external links"
],
"Structure, formatting, and lead section": [
"intro-missing",
"intro-rewrite",
"intro-tooshort",
"restructure",
"sections",
"very long"
],
"Fiction-related cleanup": [
"fiction",
"in-universe",
"plot"
]
},
"General content issues": {
"Importance and notability": [
"notability" // has subcategories and special-cased code
],
"Style of writing": [
"advertisement",
"fansite",
"jargon",
"prose",
"redlinks",
"tone"
],
"Sense (or lack thereof)": [
"confusing"
],
"Information and detail": [
"context",
"expert-subject",
"metricate"
],
"Timeliness": [
"update"
],
"Neutrality, bias, and factual accuracy": [
"autobiography",
"COI",
"disputed",
"hoax",
"globalise",
"NPOV",
"weasel"
],
"Verifiability and sources": [
"BLP sources",
"BLP unsourced",
"more sources",
"no sources",
"one source",
"original research",
"primary sources",
"self-published",
"unreliable sources"
]
},
"Specific content issues": {
"Language": [
"complex"
],
"Links": [
"dead end",
"orphan",
"wikify" // this tag is listed twice because it used to focus mainly on links, but now it's a more general cleanup tag
],
"Referencing technique": [
"citation style",
"link rot",
"more footnotes",
"no footnotes"
],
"Categories": [
"uncat"
]
},
"Merging": [
"merge",
],
"Informational": [
"in use",
"under construction"
]
};
// Tags for REDIRECTS start here
Twinkle.tag.spellingList = [
{
label: '{{R from capitalization}}: redirect from a from a capitalized title',
value: 'R from capitalization'
},
{
label: '{{R with other capitalizations}}: redirect from a title with a different capitalization',
value: 'R with other capitalizations'
},
{
label: '{{R from other name}}: redirect from a title with a different name',
value: 'R from other name'
},
{
label: '{{R from other spelling}}: redirect from a title with a different spelling',
value: 'R from other spelling'
},
{
label: '{{R from plural}}: redirect from a plural title',
value: 'R from plural'
},
{
label: '{{R from related things}}: redirect related title',
value: 'R from related things'
},
{
label: '{{R to section}}: redirect from a title for a "minor topic or title" to a comprehensive-type article section which covers the subject',
value: 'R to section'
},
{
label: '{{R from shortcut}}: redirect to a Wikipedia "shortcut"',
value: 'R from shortcut'
},
{
label: '{{R from title without diacritics}}: redirect to the article title with diacritical marks (accents, umlauts, etc.)',
value: 'R from title without diacritics'
}
];
// Contains those article tags that *do not* work inside {{multiple issues}}.
Twinkle.tag.multipleIssuesExceptions = [
'cat improve',
'in use',
'merge',
'merge from',
'merge to',
'not English',
'rough translation',
'uncat',
'under construction',
];
Twinkle.tag.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters(),
tagRe, tagText = '', summaryText = 'Added',
tags = [], groupableTags = [], i, totalTags
var pageText = pageobj.getPageText();
var addTag = function friendlytagAddTag( tagIndex, tagName ) {
var currentTag = "";
if( tagName === 'globalize' ) {
currentTag += '{{' + params.globalizeSubcategory;
} else {
currentTag += ( Twinkle.tag.mode === 'redirect' ? '\n' : '' ) + '{{' + tagName;
}
if( tagName === 'notability' && params.notabilitySubcategory !== 'none' ) {
currentTag += '|' + params.notabilitySubcategory;
}
// prompt for other parameters, based on the tag
switch( tagName ) {
case 'cleanup':
var reason = prompt('"The specific problem is: " \n' +
"This information is optional. Just click OK if you don't wish to enter this.", "");
if (reason === null) {
Morebits.status.warn("Notice", "{{cleanup}} tag skipped by user");
return true; // continue to next tag
} else {
currentTag += '|reason=' + reason;
}
break;
case 'complex':
var cpreason = prompt('"An editor’s reason for this is:" (e.g. "words like XX") \n' +
"Just click OK if you don't wish to enter this. To skip the {{complex}} tag, click Cancel.", "");
if (cpreason === null) {
return true; // continue to next tag
} else if (cpreason !== "") {
currentTag += '|2=' + cpreason;
}
break;
case 'copyedit':
var cereason = prompt('"This article may require copy editing for..." (e.g. "consistent spelling") \n' +
"Just click OK if you don't wish to enter this. To skip the {{copyedit}} tag, click Cancel.", "");
if (cereason === null) {
return true; // continue to next tag
} else if (cereason !== "") {
currentTag += '|for=' + cereason;
}
break;
case 'expert-subject':
var esreason = prompt('"This is because..." \n' +
"This information is optional. To skip the {{expert-subject}} tag, click Cancel.", "");
if (esreason === null) {
return true; // continue to next tag
} else if (esreason !== "") {
currentTag += '|1=' + esreason;
}
break;
case 'not English':
var langname = prompt('Please enter the name of the language the article is thought to be written in. \n' +
"Just click OK if you don't know. To skip the {{not English}} tag, click Cancel.", "");
if (langname === null) {
return true; // continue to next tag
} else if (langname !== "") {
currentTag += '|1=' + langname;
}
break;
case 'rough translation':
var roughlang = prompt('Please enter the name of the language the article is thought to have been translated from. \n' +
"Just click OK if you don't know. To skip the {{rough translation}} tag, click Cancel.", "");
if (roughlang === null) {
return true; // continue to next tag
} else if (roughlang !== "") {
currentTag += '|1=' + roughlang;
}
break;
case 'wikify':
var wreason = prompt('You can optionally enter a more specific reason why the article needs to be wikified: This article needs to be wikified. {{{Your reason here}}} \n' +
"Just click OK if you don't wish to enter this. To skip the {{wikify}} tag, click Cancel.", "");
if (wreason === null) {
return true; // continue to next tag
} else if (wreason !== "") {
currentTag += '|reason=' + wreason;
}
break;
case 'merge':
case 'merge to':
case 'merge from':
var param = prompt('Please enter the name of the other article(s) involved in the merge. \n' +
"To specify multiple articles, separate them with a vertical pipe (|) character. \n" +
"This information is required. Click OK when done, or click Cancel to skip the merge tag.", "");
if (param === null) {
return true; // continue to next tag
} else if (param !== "") {
currentTag += '|' + param;
}
break;
default:
break;
}
currentTag += (Twinkle.tag.mode === 'redirect') ? '}}' : '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n';
tagText += currentTag;
if ( tagIndex > 0 ) {
if( tagIndex === (totalTags - 1) ) {
summaryText += ' and';
} else if ( tagIndex < (totalTags - 1) ) {
summaryText += ',';
}
}
summaryText += ' {{[[';
summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName));
summaryText += ']]}}';
};
if( Twinkle.tag.mode !== 'redirect' ) {
// Check for preexisting tags and separate tags into groupable and non-groupable arrays
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
if( Twinkle.tag.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) {
groupableTags = groupableTags.concat( params.tags[i] );
} else {
tags = tags.concat( params.tags[i] );
}
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the article already...excluding' );
}
}
if( params.group && groupableTags.length >= 3 ) {
Morebits.status.info( 'Info', 'Grouping supported tags inside {{multiple issues}}' );
groupableTags.sort();
tagText += '{{multiple issues|\n';
totalTags = groupableTags.length;
$.each(groupableTags, addTag);
summaryText += ' tags (within {{[[Template:multiple issues|multiple issues]]}})';
if( tags.length > 0 ) {
summaryText += ', and';
}
tagText += '}}\n';
} else {
tags = tags.concat( groupableTags );
}
} else {
// Redirect tagging: Check for pre-existing tags
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
tags = tags.concat( params.tags[i] );
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the redirect already...excluding' );
}
}
}
tags.sort();
totalTags = tags.length;
$.each(tags, addTag);
if( Twinkle.tag.mode === 'redirect' ) {
pageText += tagText;
} else {
// smartly insert the new tags after any hatnotes. Regex is a bit more
// complicated than it'd need to be, to allow templates as parameters,
// and to handle whitespace properly.
pageText = pageText.replace(/^\s*(?:((?:\s*\{\{\s*(?:about|correct title|dablink|distinguish|for|other\s?(?:hurricaneuses|people|persons|places|uses(?:of)?)|redirect(?:-acronym)?|see\s?(?:also|wiktionary)|selfref|the)\d*\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\})+(?:\s*\n)?)\s*)?/i,
"$1" + tagText);
}
summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) +
' to ' + Twinkle.tag.mode + Twinkle.getPref('summaryAd');
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) {
pageobj.patrol();
}
},
file: function friendlytagCallbacksFile(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var summary = "Adding ";
// Add maintenance tags
if (params.tags.length) {
var tagtext = "", currentTag;
$.each(params.tags, function(k, tag) {
currentTag += "}}\n";
tagtext += currentTag;
summary += "{{" + tag + "}}, ";
return true; // continue
});
if (!tagtext) {
pageobj.getStatusElement().warn("User canceled operation; nothing to do");
return;
}
text = tagtext + text;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary.substring(0, summary.length - 2) + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) {
pageobj.patrol();
}
}
};
Twinkle.tag.callback.evaluate = function friendlytagCallbackEvaluate(e) {
var form = e.target;
var params = {};
switch (Twinkle.tag.mode) {
case 'article':
params.tags = form.getChecked( 'articleTags' );
params.group = form.group.checked;
params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null;
break;
case 'file':
params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null;
params.tags = form.getChecked( 'imageTags' );
break;
case 'redirect':
params.tags = form.getChecked( 'redirectTags' );
break;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
if( !params.tags.length ) {
alert( 'You must select at least one tag!' );
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds";
if (Twinkle.tag.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.tag.mode);
wikipedia_page.setCallbackParameters(params);
switch (Twinkle.tag.mode) {
case 'article':
/* falls through */
case 'redirect':
wikipedia_page.load(Twinkle.tag.callbacks.main);
return;
case 'file':
wikipedia_page.load(Twinkle.tag.callbacks.file);
return;
default:
alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode);
break;
}
};
/*
****************************************
*** twinklestub.js: Tag module
****************************************
* Mode of invocation: Tab ("Stub")
* Active on: Existing articles
* Config directives in: FriendlyConfig
* Note: customised friendlytag module (for SEWP)
*/
Twinkle.stub = function friendlytag() {
// redirect tagging
if( Morebits.wiki.isPageRedirect() ) {
Twinkle.stub.mode = 'redirect';
}
// file tagging
else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) {
Twinkle.stub.mode = 'file';
}
// article/draft article tagging
else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) {
Twinkle.stub.mode = 'article';
//twAddPortletLink( Twinkle.stub.callback, "Stub", "friendly-tag", "Add stub tags to article" );
}
};
Twinkle.stub.callback = function friendlytagCallback( uid ) {
var Window = new Morebits.simpleWindow( 630, (Twinkle.stub.mode === "article") ? 450 : 400 );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Simple Stub project", "Wikipedia:Simple Stub Project" );
Window.addFooterLink( "Stub guideline", "Wikipedia:Stub" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#stub" );
var form = new Morebits.quickForm( Twinkle.stub.callback.evaluate );
switch( Twinkle.stub.mode ) {
case 'article':
Window.setTitle( "Article stub tagging" );
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (https://dev.miraheze.org/wiki/Twinkle/Preferences).',
event: Twinkle.stub.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'alpha' }
]
});
form.append( { type: 'div', id: 'tagWorkArea' } );
}
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
if (Twinkle.stub.mode === "article") {
// fake a change event on the sort dropdown, to initialize the tag list
var evt = document.createEvent("Event");
evt.initEvent("change", true, true);
result.sortorder.dispatchEvent(evt);
}
};
Twinkle.stub.checkedTags = [];
Twinkle.stub.updateSortOrder = function(e) {
var sortorder = e.target.value;
var $workarea = $(e.target.form).find("div#tagWorkArea");
Twinkle.stub.checkedTags = e.target.form.getChecked("articleTags");
if (!Twinkle.stub.checkedTags) {
Twinkle.stub.checkedTags = [];
}
// function to generate a checkbox, with appropriate subgroup if needed
var makeCheckbox = function(tag, description) {
var checkbox = { value: tag, label: "{{" + tag + "}}: " + description };
if (Twinkle.stub.checkedTags.indexOf(tag) !== -1) {
checkbox.checked = true;
}
return checkbox;
};
// categorical sort order
if (sortorder === "cat") {
var div = new Morebits.quickForm.element({
type: "div",
id: "tagWorkArea"
});
// function to iterate through the tags and create a checkbox for each one
var doCategoryCheckboxes = function(subdiv, array) {
var checkboxes = [];
$.each(array, function(k, tag) {
var description = Twinkle.stub.article.tags[tag];
checkboxes.push(makeCheckbox(tag, description));
});
subdiv.append({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
};
var i = 0;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.stub.article.tagCategories, function(title, content) {
div.append({ type: "header", id: "tagHeader" + i, label: title });
var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ });
if ($.isArray(content)) {
doCategoryCheckboxes(subdiv, content);
} else {
$.each(content, function(subtitle, subcontent) {
subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] });
doCategoryCheckboxes(subdiv, subcontent);
});
}
});
var rendered = div.render();
$workarea.replaceWith(rendered);
var $rendered = $(rendered);
$rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' });
$rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' });
}
// alphabetical sort order
else {
var checkboxes = [];
$.each(Twinkle.stub.article.tags, function(tag, description) {
checkboxes.push(makeCheckbox(tag, description));
});
var tags = new Morebits.quickForm.element({
type: "checkbox",
name: "articleTags",
list: checkboxes
});
$workarea.empty().append(tags.render());
}
};
// Tags for ARTICLES start here
Twinkle.stub.article = {};
// A list of all article tags, in alphabetical order
// To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below.
Twinkle.stub.article.tags = {
"actor-stub": "for use with articles about actors",
"asia-stub": "for use with anything about Asia, except people",
"bio-stub": "for use with all people, no matter who or what profession",
"biology-stub": "for use with topics related to biology",
"chem-stub": "for use with topics related to chemistry",
"europe-stub": "for use with anything about Europe, except people",
"france-geo-stub": "for use with France geography topics",
"food-stub": "for use with anything about food",
"geo-stub": "for use with all geographical locations (places, towns, cities, etc)",
"history-stub": "for use with history topics",
"japan-stub": "for use with anything about Japan, except people",
"japan-sports-bio-stub": "for use with Japanese sport biographies",
"list-stub": "for use with lists only",
"lit-stub": "for use with all literature articles except people",
"math-stub": "for use with topics related to mathematics",
"med-stub": "for use with topics related to medicine",
"military-stub": "for use with military related topics",
"movie-stub": "for use with all movie articles except people",
"music-stub": "for use with all music articles except people",
"north-America-stub": "for use with anything about North America, except people",
"performing-arts-stub": "general stub for the performing arts",
"physics-stub": "for use with topics related to physics",
"politics-stub": "for use with politics related topics",
"religion-stub": "for use with religion related topics",
"sci-stub": "anything science related (all branches and their tools)",
"sport-stub": "general stub for all sports and sports items, not people",
"sports-bio-stub": "for use with people who have sport as profession",
"stub": "for all stubs that can not fit into any stub we have",
"switzerland-stub": "for use with everything about Switzerland, except people",
"tech-stub": "for use with technology related articles",
"transport-stub": "for use with articles about any moving object (cars, bikes, ships, crafts, planes, rail, buses, trains, etc)",
"tv-stub": "for use with all television articles except people",
"UK-stub": "for use with anything about the United Kingdom, except people",
"US-actor-stub": "for use with United States actor biographies",
"US-bio-stub": "for use with United States biographies",
"US-geo-stub": "for use with United States geography topics",
"US-stub": "for use with anything about the United States, except people and geography",
"video-game-stub": "for use with stubs related to video games",
"weather-stub": "for articles about weather"
};
// A list of tags in order of category
// Tags should be in alphabetical order within the categories
// Add new categories with discretion - the list is long enough as is!
Twinkle.stub.article.tagCategories = {
"Stub templates": [
"stub",
"list-stub"
],
"Countries & Geography": [
"asia-stub",
"europe-stub",
"france-geo-stub",
"geo-stub",
"japan-stub",
"japan-sports-bio-stub",
"north-America-stub",
"switzerland-stub",
"UK-stub",
"US-bio-stub",
"US-geo-stub",
"US-stub"
],
"Miscellaneous": [
"food-stub",
"history-stub",
"military-stub",
"politics-stub",
"religion-stub",
"transport-stub"
],
"People": [
"actor-stub",
"bio-stub",
"japan-sports-bio-stub",
"sports-bio-stub",
"US-actor-stub",
"US-bio-stub"
],
"Science": [
"biology-stub",
"chem-stub",
"math-stub",
"med-stub",
"physics-stub",
"sci-stub",
"weather-stub"
],
"Sports": [
"japan-sports-bio-stub",
"sport-stub",
"sports-bio-stub"
],
"Technology": [
"tech-stub",
"video-game-stub"
],
"Arts": [
"actor-stub",
"lit-stub",
"movie-stub",
"music-stub",
"performing-arts-stub",
"tv-stub",
"US-actor-stub"
]
}
// Tags for REDIRECTS start here
// Contains those article tags that *do not* work inside {{multiple issues}}.
Twinkle.stub.multipleIssuesExceptions = [
'cat improve',
'in use',
'merge',
'merge from',
'merge to',
'not English',
'rough translation',
'uncat',
'under construction',
'update'
];
Twinkle.stub.callbacks = {
main: function( pageobj ) {
var params = pageobj.getCallbackParameters(),
tagRe, tagText = '', summaryText = 'Added',
tags = [], groupableTags = [], i, totalTags;
// Remove tags that become superfluous with this action
var pageText = pageobj.getPageText();
var addTag = function friendlytagAddTag( tagIndex, tagName ) {
var currentTag = "";
pageText += '\n\n{{' + tagName + '}}';
if ( tagIndex > 0 ) {
if( tagIndex === (totalTags - 1) ) {
summaryText += ' and';
} else if ( tagIndex < (totalTags - 1) ) {
summaryText += ',';
}
}
summaryText += ' {{[[';
summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName));
summaryText += ']]}}';
};
// Check for preexisting tags and separate tags into groupable and non-groupable arrays
for( i = 0; i < params.tags.length; i++ ) {
tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' );
if( !tagRe.exec( pageText ) ) {
if( Twinkle.stub.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) {
groupableTags = groupableTags.concat( params.tags[i] );
} else {
tags = tags.concat( params.tags[i] );
}
} else {
Morebits.status.info( 'Info', 'Found {{' + params.tags[i] +
'}} on the article already...excluding' );
}
}
tags = tags.concat( groupableTags );
tags.sort();
totalTags = tags.length;
$.each(tags, addTag);
summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) +
' to ' + Twinkle.stub.mode + Twinkle.getPref('summaryAd');
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchStubbedPages'));
pageobj.setMinorEdit(Twinkle.getFriendlyPref('markStubbedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if( Twinkle.getFriendlyPref('markStubbedPagesAsPatrolled') ) {
pageobj.patrol();
}
}
};
Twinkle.stub.callback.evaluate = function friendlytagCallbackEvaluate(e) {
var form = e.target;
var params = {};
switch (Twinkle.stub.mode) {
case 'article':
params.tags = form.getChecked( 'articleTags' );
params.group = false;
params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null;
break;
case 'file':
params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null;
params.tags = form.getChecked( 'imageTags' );
break;
case 'redirect':
params.tags = form.getChecked( 'redirectTags' );
break;
}
if( !params.tags.length ) {
alert( 'You must select at least one tag!' );
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds";
if (Twinkle.stub.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.stub.mode);
wikipedia_page.setCallbackParameters(params);
switch (Twinkle.stub.mode) {
case 'article':
/* falls through */
case 'redirect':
wikipedia_page.load(Twinkle.stub.callbacks.main);
return;
case 'file':
wikipedia_page.load(Twinkle.stub.callbacks.file);
return;
}
};
/*
****************************************
*** friendlytalkback.js: Talkback module
****************************************
* Mode of invocation: Tab ("TB")
* Active on: Existing user talk pages
* Config directives in: FriendlyConfig
*/
;(function(){
Twinkle.talkback = function() {
if ( Morebits.getPageAssociatedUser() === false ) {
return;
}
twAddPortletLink( callback, "TB", "friendly-talkback", "Easy talkback" );
};
var callback = function( ) {
if( Morebits.getPageAssociatedUser() === mw.config.get("wgUserName") && !confirm("Is it really so bad that you're talking back to yourself?") ){
return;
}
var Window = new Morebits.simpleWindow( 600, 350 );
Window.setTitle("Talkback");
Window.setScriptName("Twinkle");
Window.addFooterLink( "About {{talkback}}", "Template:Talkback" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#talkback" );
var form = new Morebits.quickForm( callback_evaluate );
form.append({ type: "radio", name: "tbtarget",
list: [
{
label: "Talkback: my talk page",
value: "mytalk",
checked: "true"
},
{
label: "Talkback: other user talk page",
value: "usertalk"
},
{
label: "Talkback: other page",
value: "other"
},
{
label: "Noticeboard notification",
value: "notice"
},
{
label: "\"You've got mail\"",
value: "mail"
},
{
label: "Whisperback",
value: "wb"
}
],
event: callback_change_target
});
form.append({
type: "field",
label: "Work area",
name: "work_area"
});
form.append({ type: "submit" });
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the
var evt = document.createEvent("Event");
evt.initEvent( "change", true, true );
result.tbtarget[0].dispatchEvent( evt );
};
var prev_page = "";
var prev_section = "";
var prev_message = "";
var callback_change_target = function( e ) {
var value = e.target.values;
var root = e.target.form;
var old_area = Morebits.quickForm.getElements(root, "work_area")[0];
if(root.section) {
prev_section = root.section.value;
}
if(root.message) {
prev_message = root.message.value;
}
if(root.page) {
prev_page = root.page.value;
}
var work_area = new Morebits.quickForm.element({
type: "field",
label: "Talkback information",
name: "work_area"
});
switch( value ) {
case "mytalk":
/* falls through */
default:
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on your talk page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "usertalk":
work_area.append({
type:"input",
name:"page",
label:"User",
tooltip:"The username of the user on whose talk page you left a message.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "notice":
var noticeboard = work_area.append({
type: "select",
name: "noticeboard",
label: "Noticeboard:"
});
noticeboard.append({
type: "option",
label: "WP:AN (Administrators' noticeboard)",
value: "an"
});
work_area.append({
type:"input",
name:"section",
label:"Linked thread",
tooltip:"The heading of the relevant thread on the noticeboard page.",
value: prev_section
});
break;
case "other":
work_area.append({
type:"input",
name:"page",
label:"Full page name",
tooltip:"The full page name where you left the message. For example: 'Wikipedia talk:Twinkle'.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
case "mail":
work_area.append({
type:"input",
name:"section",
label:"Subject of e-mail (optional)",
tooltip:"The subject line of the e-mail you sent."
});
break;
case "wb":
work_area.append({
type:"input",
name:"page",
label:"User",
tooltip:"The username of the user on whose talk page you left a message.",
value: prev_page
});
work_area.append({
type:"input",
name:"section",
label:"Linked section (optional)",
tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.",
value: prev_section
});
break;
}
if (value !== "notice") {
work_area.append({ type:"textarea", label:"Additional message (optional):", name:"message", tooltip:"An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one." });
}
work_area = work_area.render();
root.replaceChild( work_area, old_area );
if (root.message) {
root.message.value = prev_message;
}
};
var callback_evaluate = function( e ) {
var tbtarget = e.target.getChecked( "tbtarget" )[0];
var page = null;
var section = e.target.section.value;
var fullUserTalkPageName = mw.config.get("wgFormattedNamespaces")[ mw.config.get("wgNamespaceIds").user_talk ] + ":" + Morebits.getPageAssociatedUser();
if( tbtarget === "usertalk" || tbtarget === "other" || tbtarget === "wb" ) {
page = e.target.page.value;
if( tbtarget === "usertalk" ) {
if( !page ) {
alert("You must specify the username of the user whose talk page you left a message on.");
return;
}
} else {
if( !page ) {
alert("You must specify the full page name when your message is not on a user talk page.");
return;
}
}
} else if (tbtarget === "notice") {
page = e.target.noticeboard.value;
}
var message;
if (e.target.message) {
message = e.target.message.value;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName;
Morebits.wiki.actionCompleted.notice = "Talkback complete; reloading talk page in a few seconds";
var talkpage = new Morebits.wiki.page(fullUserTalkPageName, "Adding talkback");
var tbPageName = (tbtarget === "mytalk") ? mw.config.get("wgUserName") : page;
var text;
if ( tbtarget === "notice" ) {
text = "\n\n== " + Twinkle.getFriendlyPref("adminNoticeHeading") + " ==\n";
text += "{{subst:AN-notice|thread=" + section + "|noticeboard=Wikipedia:Administrators' noticeboard}} ~~~~";
talkpage.setEditSummary( "Notice of discussion at [[Wikipedia:Administrators' noticeboard]]" + Twinkle.getPref("summaryAd") );
} else if ( tbtarget === "mail" ) {
text = "\n\n==" + Twinkle.getFriendlyPref("mailHeading") + "==\n{{you've got mail|subject=";
text += section + "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Notification: You've got mail" + Twinkle.getPref("summaryAd"));
} else if ( tbtarget === "wb" ) {
text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{wb|";
text += tbPageName;
if( section ) {
text += "|" + section;
}
text += "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Whisperback" + Twinkle.getPref("summaryAd"));
} else {
//clean talkback heading: strip section header markers, were erroneously suggested in the documentation
text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{tb|";
text += tbPageName;
if( section ) {
text += "|" + section;
}
text += "|ts=~~~~~}}";
if( message ) {
text += "\n" + message + " ~~~~";
} else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) {
text += "\n~~~~";
}
talkpage.setEditSummary("Talkback ([[" + (tbtarget === "other" ? "" : "User talk:") + tbPageName +
(section ? ("#" + section) : "") + "]])" + Twinkle.getPref("summaryAd"));
}
talkpage.setAppendText( text );
talkpage.setCreateOption("recreate");
talkpage.setMinorEdit(Twinkle.getFriendlyPref("markTalkbackAsMinor"));
talkpage.setFollowRedirect( true );
talkpage.append();
};
}());
/*
****************************************
*** friendlywelcome.js: Welcome module
****************************************
* Mode of invocation: Tab ("Wel"), or from links on diff pages
* Active on: Existing user talk pages, diff pages
* Config directives in: FriendlyConfig
*/
Twinkle.welcome = function friendlywelcome() {
if( Morebits.queryString.exists( 'friendlywelcome' ) ) {
if( Morebits.queryString.get( 'friendlywelcome' ) === 'auto' ) {
Twinkle.welcome.auto();
} else {
Twinkle.welcome.semiauto();
}
} else {
Twinkle.welcome.normal();
}
};
Twinkle.welcome.auto = function() {
if( Morebits.queryString.get( 'action' ) !== 'edit' ) {
// userpage not empty, aborting auto-welcome
return;
}
Twinkle.welcome.welcomeUser();
};
Twinkle.welcome.semiauto = function() {
Twinkle.welcome.callback( mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\"") );
};
Twinkle.welcome.normal = function() {
if( Morebits.queryString.exists( 'diff' ) ) {
// check whether the contributors' talk pages exist yet
var $oList = $("#mw-diff-otitle2").find("span.mw-usertoollinks a.new:contains(talk)").first();
var $nList = $("#mw-diff-ntitle2").find("span.mw-usertoollinks a.new:contains(talk)").first();
if( $oList.length > 0 || $nList.length > 0 ) {
var spanTag = function( color, content ) {
var span = document.createElement( 'span' );
span.style.color = color;
span.appendChild( document.createTextNode( content ) );
return span;
};
var welcomeNode = document.createElement('strong');
var welcomeLink = document.createElement('a');
welcomeLink.appendChild( spanTag( 'Black', '[' ) );
welcomeLink.appendChild( spanTag( 'Goldenrod', 'welcome' ) );
welcomeLink.appendChild( spanTag( 'Black', ']' ) );
welcomeNode.appendChild(welcomeLink);
if( $oList.length > 0 ) {
var oHref = $oList.attr("href");
var oWelcomeNode = welcomeNode.cloneNode( true );
oWelcomeNode.firstChild.setAttribute( 'href', oHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) );
$oList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) );
$oList[0].parentNode.parentNode.appendChild( oWelcomeNode );
}
if( $nList.length > 0 ) {
var nHref = $nList.attr("href");
var nWelcomeNode = welcomeNode.cloneNode( true );
nWelcomeNode.firstChild.setAttribute( 'href', nHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) );
$nList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) );
$nList[0].parentNode.parentNode.appendChild( nWelcomeNode );
}
}
}
if( mw.config.get( 'wgNamespaceNumber' ) === 3 ) {
var username = mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes
twAddPortletLink( function(){ Twinkle.welcome.callback(username); }, "Wel", "friendly-welcome", "Welcome user" );
}
};
Twinkle.welcome.welcomeUser = function welcomeUser() {
Morebits.status.init( document.getElementById('bodyContent') );
var params = {
value: Twinkle.getFriendlyPref('quickWelcomeTemplate'),
article: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '',
mode: 'auto'
};
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.welcome.callbacks.main);
};
Twinkle.welcome.callback = function friendlywelcomeCallback( uid ) {
if( uid === mw.config.get('wgUserName') && !confirm( 'Are you really sure you want to welcome yourself?...' ) ){
return;
}
var Window = new Morebits.simpleWindow( 600, 420 );
Window.setTitle( "Welcome user" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#welcome" );
var form = new Morebits.quickForm( Twinkle.welcome.callback.evaluate );
form.append({
type: 'select',
name: 'type',
label: 'Type of welcome: ',
event: Twinkle.welcome.populateWelcomeList,
list: [
{ type: 'option', value: 'standard', label: 'Standard welcomes', selected: !Morebits.isIPAddress(mw.config.get('wgTitle')) },
{ type: 'option', value: 'anonymous', label: 'Problem user welcomes', selected: Morebits.isIPAddress(mw.config.get('wgTitle')) }
]
});
form.append( { type: 'div', id: 'welcomeWorkArea' } );
form.append( {
type: 'input',
name: 'article',
label: '* Linked article (if supported by template):',
value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ),
tooltip: 'An article might be linked from within the welcome if the template supports it. Leave empty for no article to be linked. Templates that support a linked article are marked with an asterisk.'
} );
var previewlink = document.createElement( 'a' );
$(previewlink).click(function(){
Twinkle.welcome.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = "pointer";
previewlink.textContent = 'Preview';
form.append( { type: 'div', name: 'welcomepreview', label: [ previewlink ] } );
form.append( { type: 'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
// initialize the welcome list
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.type.dispatchEvent( evt );
};
Twinkle.welcome.populateWelcomeList = function(e) {
var type = e.target.value;
var $workarea = $(e.target.form).find("div#welcomeWorkArea");
var div = new Morebits.quickForm.element({
type: "div",
id: "welcomeWorkArea"
});
if ((type === "standard" || type === "anonymous") && Twinkle.getFriendlyPref("customWelcomeList").length) {
div.append({ type: 'header', label: 'Custom welcome templates' });
div.append({
type: 'radio',
name: 'template',
list: Twinkle.getFriendlyPref("customWelcomeList"),
event: Twinkle.welcome.selectTemplate
});
}
var appendTemplates = function(list) {
div.append({
type: 'radio',
name: 'template',
list: list.map(function(obj) {
var properties = Twinkle.welcome.templates[obj];
var result = (properties ? {
value: obj,
label: "{{" + obj + "}}: " + properties.description + (properties.linkedArticle ? "\u00A0*" : ""), // U+00A0 NO-BREAK SPACE
tooltip: properties.tooltip // may be undefined
} : {
value: obj,
label: "{{" + obj + "}}"
});
return result;
}),
event: Twinkle.welcome.selectTemplate
});
};
switch (type) {
case "standard":
div.append({ type: 'header', label: 'General welcome templates' });
appendTemplates([
"welcome",
"welcome2",
"welcome-anon",
"welcome-anon2",
"welcome-en",
"welcome-iw",
"welcomeg",
"welcomeq",
"welcome-personal",
"welcome-school"
]);
break;
case "anonymous":
div.append({ type: 'header', label: 'Problem user welcome templates' });
appendTemplates([
"firstarticle",
"welcomespam",
"welcomenpov",
"welcomevandal"
]);
break;
default:
div.append({ type: 'div', label: 'Twinkle.welcome.populateWelcomeList: something went wrong' });
break;
}
var rendered = div.render();
rendered.className = "quickform-scrollbox";
$workarea.replaceWith(rendered);
var firstRadio = e.target.form.template[0];
firstRadio.checked = true;
Twinkle.welcome.selectTemplate({ target: firstRadio });
};
Twinkle.welcome.selectTemplate = function(e) {
var properties = Twinkle.welcome.templates[e.target.values];
e.target.form.article.disabled = (properties ? !properties.linkedArticle : false);
};
// A list of welcome templates and their properties and syntax
// The four fields that are available are "description", "linkedArticle", "syntax", and "tooltip".
// The three magic words that can be used in the "syntax" field are:
// - $USERNAME$ - replaced by the welcomer's username, depending on user's preferences
// - $ARTICLE$ - replaced by an article name, if "linkedArticle" is true
// - $HEADER$ - adds a level 2 header (most templates already include this)
Twinkle.welcome.templates = {
"welcome": {
description: "standard plain text welcome",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome2": {
description: "welcome with graphic and orange color sheme",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome2|~~~~|art=$ARTICLE$}}"
},
"welcome-anon": {
description: "welcome anonymous user and suggest getting a username",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome-anon|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-anon2": {
description: "like welcome-anon, but with table and colors",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcome-anon2|$USERNAME$|art=$ARTICLE$}}"
},
"welcome-en": {
description: "welcome for users from main English Wikipedia",
linkedArticle: false,
syntax: "$HEADER$ {{subst:welcome-en}} ~~~~"
},
"welcome-iw": {
description: "welcome users from another Wikipedia",
linkedArticle: false,
syntax: "$HEADER$ {{subst:welcome-iw}} ~~~~"
},
"welcomeg": {
description: "welcome with blue background",
linkedArticle: true,
syntax: "{{subst:welcomeg|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcomeq": {
description: "like welcomeg but a bit shorter",
linkedArticle: true,
syntax: "{{subst:welcomeq|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-personal": {
description: "a more personal welcome with a plate of cookies",
linkedArticle: true,
syntax: "{{subst:welcome-personal|$USERNAME$|art=$ARTICLE$}} ~~~~"
},
"welcome-school": {
description: "for welcoming students participating in a class project",
linkedArticle: false,
syntax: "{{subst:welcome-school}} ~~~~"
},
// second group
"firstarticle": {
description: "welcome with note that created page may get deleted",
linkedArticle: true,
syntax: "$HEADER$ {{subst:firstarticle|1=$ARTICLE$}} ~~~~"
},
"welcomespam": {
description: "welcome users which did spam changes",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomespam|art=$ARTICLE$}} ~~~~"
},
"welcomenpov": {
description: "welcome with warning to make changes that fit the NPOV requirements",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomenpov|$ARTICLE$}} ~~~~"
},
"welcomevandal": {
description: "welcome user which performed vandalism",
linkedArticle: true,
syntax: "$HEADER$ {{subst:welcomevandal|$ARTICLE$}} ~~~~"
}
};
Twinkle.welcome.getTemplateWikitext = function(template, article) {
var properties = Twinkle.welcome.templates[template];
if (properties) {
return properties.syntax.
replace("$USERNAME$", Twinkle.getFriendlyPref("insertUsername") ? mw.config.get("wgUserName") : "").
replace("$ARTICLE$", article ? article : "").
replace(/\$HEADER\$\s*/, "== Welcome ==\n\n").
replace("$EXTRA$", ""); // EXTRA is not implemented yet
} else {
return "{{subst:" + template + (article ? ("|art=" + article) : "") + "}} ~~~~";
}
};
Twinkle.welcome.callbacks = {
preview: function(form) {
var previewDialog = new Morebits.simpleWindow(750, 400);
previewDialog.setTitle("Welcome template preview");
previewDialog.setScriptName("Welcome user");
previewDialog.setModality(true);
var previewdiv = document.createElement("div");
previewdiv.style.marginLeft = previewdiv.style.marginRight = "0.5em";
previewdiv.style.fontSize = "small";
previewDialog.setContent(previewdiv);
var previewer = new Morebits.wiki.preview(previewdiv);
previewer.beginRender(Twinkle.welcome.getTemplateWikitext(form.getChecked("template"), form.article.value));
var submit = document.createElement("input");
submit.setAttribute("type", "submit");
submit.setAttribute("value", "Close");
previewDialog.addContent(submit);
previewDialog.display();
$(submit).click(function(e) {
previewDialog.close();
});
},
main: function( pageobj ) {
var params = pageobj.getCallbackParameters();
var text = pageobj.getPageText();
// abort if mode is auto and form is not empty
if( pageobj.exists() && params.mode === 'auto' ) {
Morebits.status.info( 'Warning', 'User talk page not empty; aborting automatic welcome' );
Morebits.wiki.actionCompleted.event();
return;
}
var welcomeText = Twinkle.welcome.getTemplateWikitext(params.value, params.article);
if( Twinkle.getFriendlyPref('topWelcomes') ) {
text = welcomeText + '\n\n' + text;
} else {
text += "\n" + welcomeText;
}
var summaryText = "Welcome to Wikipedia!";
pageobj.setPageText(text);
pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(Twinkle.getFriendlyPref('watchWelcomes'));
pageobj.setCreateOption('recreate');
pageobj.save();
}
};
Twinkle.welcome.callback.evaluate = function friendlywelcomeCallbackEvaluate(e) {
var form = e.target;
var params = {
value: form.getChecked("template"),
article: form.article.value,
mode: 'manual'
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.welcome.callbacks.main);
};
/*
****************************************
*** twinklearv.js: ARV module
****************************************
* Mode of invocation: Tab ("ARV")
* Active on: Existing and non-existing user pages, user talk pages, contributions pages
* Config directives in: TwinkleConfig
*/
Twinkle.arv = function twinklearv() {
var username = Morebits.getPageAssociatedUser();
if ( username === false ) {
return;
}
var title = Morebits.isIPAddress( username ) ? 'Report IP to administrators' : 'Report user to administrators';
twAddPortletLink( function(){ Twinkle.arv.callback(username); }, "VIP", "tw-arv", title );
};
Twinkle.arv.callback = function ( uid ) {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
if ( uid === mw.config.get('wgUserName') ) {
alert( 'You don\'t want to report yourself, do you?' );
return;
}
var Window = new Morebits.simpleWindow( 600, 500 );
Window.setTitle( "Vandalism in progress" ); //changed title
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#arv" );
var form = new Morebits.quickForm( Twinkle.arv.callback.evaluate );
var categories = form.append( {
type: 'select',
name: 'category',
label: 'Select report type: ',
event: Twinkle.arv.callback.changeCategory
} );
categories.append( {
type: 'option',
label: 'Vandalism (WP:VIP)',
value: 'aiv'
} );
form.append( {
type: 'field',
label:'Work area',
name: 'work_area'
} );
form.append( { type:'submit' } );
form.append( {
type: 'hidden',
name: 'uid',
value: uid
} );
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.category.dispatchEvent( evt );
};
Twinkle.arv.callback.changeCategory = function (e) {
var value = e.target.value;
var root = e.target.form;
var old_area = Morebits.quickForm.getElements(root, "work_area")[0];
var work_area = null;
switch( value ) {
case 'aiv':
/* falls through */
default:
work_area = new Morebits.quickForm.element( {
type: 'field',
label: 'Report user for vandalism',
name: 'work_area'
} );
work_area.append( {
type: 'input',
name: 'page',
label: 'Primary linked page: ',
tooltip: 'Leave blank to not link to the page in the report',
value: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '',
event: function(e) {
var value = e.target.value;
var root = e.target.form;
if( value === '' ) {
root.badid.disabled = root.goodid.disabled = true;
} else {
root.badid.disabled = false;
root.goodid.disabled = root.badid.value === '';
}
}
} );
work_area.append( {
type: 'input',
name: 'badid',
label: 'Revision ID for target page when vandalised: ',
tooltip: 'Leave blank for no diff link',
value: Morebits.queryString.exists( 'vanarticlerevid' ) ? Morebits.queryString.get( 'vanarticlerevid' ) : '',
disabled: !Morebits.queryString.exists( 'vanarticle' ),
event: function(e) {
var value = e.target.value;
var root = e.target.form;
root.goodid.disabled = value === '';
}
} );
work_area.append( {
type: 'input',
name: 'goodid',
label: 'Last good revision ID before vandalism of target page: ',
tooltip: 'Leave blank for diff link to previous revision',
value: Morebits.queryString.exists( 'vanarticlegoodrevid' ) ? Morebits.queryString.get( 'vanarticlegoodrevid' ) : '',
disabled: !Morebits.queryString.exists( 'vanarticle' ) || Morebits.queryString.exists( 'vanarticlerevid' )
} );
work_area.append( {
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Vandalism after final (level 4 or 4im) warning given',
value: 'final'
},
{
label: 'Vandalism after recent (within 1 day) release of block',
value: 'postblock'
},
{
label: 'Evidently a vandalism-only account',
value: 'vandalonly',
disabled: Morebits.isIPAddress( root.uid.value )
},
{
label: 'Account is evidently a spambot or a compromised account',
value: 'spambot'
},
{
label: 'Account is a promotion-only account',
value: 'promoonly'
}
]
} );
work_area.append( {
type: 'textarea',
name: 'reason',
label: 'Comment: '
} );
work_area = work_area.render();
old_area.parentNode.replaceChild( work_area, old_area );
break;
}
};
Twinkle.arv.callback.evaluate = function(e) {
var form = e.target;
var reason = "";
var comment = "";
if ( form.reason ) {
comment = form.reason.value;
}
var uid = form.uid.value;
var types;
switch( form.category.value ) {
// Report user for vandalism
case 'aiv':
/* falls through */
default:
types = form.getChecked( 'arvtype' );
if( !types.length && comment === '' ) {
alert( 'You must specify some reason' );
return;
}
types = types.map( function(v) {
switch(v) {
case 'final':
return 'vandalism after final warning';
case 'postblock':
return 'vandalism after recent release of block';
case 'spambot':
return 'account is evidently a spambot or a compromised account';
case 'vandalonly':
return 'actions evidently indicate a vandalism-only account';
case 'promoonly':
return 'account is being used only for promotional purposes';
default:
return 'unknown reason';
}
} ).join( '; ' );
if ( form.page.value !== '' ) {
// add a leading : on linked page namespace to prevent transclusion
reason = 'On [[' + form.page.value.replace( /^(Image|Category|File):/i, ':$1:' ) + ']]';
if ( form.badid.value !== '' ) {
var query = {
'title': form.page.value,
'diff': form.badid.value,
'oldid': form.goodid.value
};
reason += ' ({{diff|' + form.page.value + '|' + form.badid.value + '|' + form.goodid.value + '|diff}})';
}
reason += ':';
}
if ( types ) {
reason += " " + types;
}
if (comment !== "" ) {
reason += (reason === "" ? "" : ". ") + comment;
}
reason += ". ~~~~";
reason = reason.replace(/\r?\n/g, "\n*:"); // indent newlines
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = "Wikipedia:Vandalism in progress";
Morebits.wiki.actionCompleted.notice = "Reporting complete";
var aivPage = new Morebits.wiki.page( 'Wikipedia:Vandalism in progress', 'Processing VIP request' );
aivPage.setPageSection( 3 );
aivPage.setFollowRedirect( true );
aivPage.load( function() {
var text = aivPage.getPageText();
// check if user has already been reported
if (new RegExp( "\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*" + RegExp.escape( uid, true ) + "\\s*\\}\\}" ).test(text)) {
aivPage.getStatusElement().info( 'Report already present, will not add a new one' );
return;
}
aivPage.getStatusElement().status( 'Adding new report...' );
aivPage.setEditSummary( 'Reporting [[Special:Contributions/' + uid + '|' + uid + ']].' + Twinkle.getPref('summaryAd') );
aivPage.setAppendText( '\n*{{' + ( Morebits.isIPAddress( uid ) ? 'IPvandal' : 'vandal' ) + '|' + (/\=/.test( uid ) ? '1=' : '' ) + uid + '}} – ' + reason );
aivPage.append();
} );
break;
}
};
/*
****************************************
*** twinklebatchdelete.js: Batch delete module (sysops only)
****************************************
* Mode of invocation: Tab ("D-batch")
* Active on: Existing and non-existing non-articles, and Special:PrefixIndex
* Config directives in: TwinkleConfig
*/
Twinkle.batchdelete = function twinklebatchdelete() {
if( Morebits.userIsInGroup( 'sysop' ) && (mw.config.get( 'wgNamespaceNumber' ) > 0 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) {
twAddPortletLink( Twinkle.batchdelete.callback, "D-batch", "tw-batch", "Delete pages found in this category/on this page" );
}
};
Twinkle.batchdelete.unlinkCache = {};
Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Batch deletion" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#batchdelete" );
var form = new Morebits.quickForm( Twinkle.batchdelete.callback.evaluate );
form.append( {
type: 'checkbox',
list: [
{
label: 'Delete pages',
name: 'delete_page',
value: 'delete',
checked: true
},
{
label: 'Remove backlinks to the page',
name: 'unlink_page',
value: 'unlink',
checked: false
},
{
label: 'Delete redirects to deleted pages',
name: 'delete_redirects',
value: 'delete_redirects',
checked: true
}
]
} );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: '
} );
var query;
if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // Category:
query = {
'action': 'query',
'generator': 'categorymembers',
'gcmtitle': mw.config.get( 'wgPageName' ),
'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': [ 'categories', 'revisions' ],
'rvprop': [ 'size' ]
};
} else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) {
var gapnamespace, gapprefix;
if(Morebits.queryString.exists( 'from' ) )
{
gapnamespace = Morebits.queryString.get( 'namespace' );
gapprefix = Morebits.string.toUpperCaseFirstChar( Morebits.queryString.get( 'from' ) );
}
else
{
var pathSplit = location.pathname.split('/');
if (pathSplit.length < 3 || pathSplit[2] !== "Special:PrefixIndex") {
return;
}
var titleSplit = pathSplit[3].split(':');
gapnamespace = mw.config.get("wgNamespaceIds")[titleSplit[0].toLowerCase()];
if ( titleSplit.length < 2 || typeof gapnamespace === 'undefined' )
{
gapnamespace = 0; // article namespace
gapprefix = pathSplit.splice(3).join('/');
}
else
{
pathSplit = pathSplit.splice(4);
pathSplit.splice(0,0,titleSplit.splice(1).join(':'));
gapprefix = pathSplit.join('/');
}
}
query = {
'action': 'query',
'generator': 'allpages',
'gapnamespace': gapnamespace ,
'gapprefix': gapprefix,
'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop' : ['categories', 'revisions' ],
'rvprop': [ 'size' ]
};
} else {
query = {
'action': 'query',
'generator': 'links',
'titles': mw.config.get( 'wgPageName' ),
'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': [ 'categories', 'revisions' ],
'rvprop': [ 'size' ]
};
}
var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//page[@ns != "6" and not(@missing)]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); // 6 = File: namespace
var list = [];
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var object = snapshot.snapshotItem(i);
var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue;
var size = xmlDoc.evaluate( 'revisions/rev/@size', object, null, XPathResult.NUMBER_TYPE, null ).numberValue;
var disputed = xmlDoc.evaluate( 'boolean(categories/cl[@title="Category:Contested candidates for speedy deletion"])', object, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue;
list.push( {label:page + ' (' + size + ' bytes)' + ( disputed ? ' (DISPUTED CSD)' : '' ), value:page, checked:!disputed });
}
self.params.form.append( {
type: 'checkbox',
name: 'pages',
list: list
} );
self.params.form.append( { type:'submit' } );
var result = self.params.form.render();
self.params.Window.setContent( result );
} );
wikipedia_api.params = { form:form, Window:Window };
wikipedia_api.post();
var root = document.createElement( 'div' );
Morebits.status.init( root );
Window.setContent( root );
Window.display();
};
Twinkle.batchdelete.currentDeleteCounter = 0;
Twinkle.batchdelete.currentUnlinkCounter = 0;
Twinkle.batchdelete.currentdeletor = 0;
Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Status';
Morebits.wiki.actionCompleted.postfix = 'batch deletion is now complete';
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
var delete_page = event.target.delete_page.checked;
var unlink_page = event.target.unlink_page.checked;
var delete_redirects = event.target.delete_redirects.checked;
if( ! reason ) {
return;
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'nothing to delete, aborting' );
return;
}
function toCall( work ) {
if( work.length === 0 && Twinkle.batchdelete.currentDeleteCounter <= 0 && Twinkle.batchdelete.currentUnlinkCounter <= 0 ) {
window.clearInterval( Twinkle.batchdelete.currentdeletor );
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && ( Twinkle.batchdelete.currentDeleteCounter <= Twinkle.getPref('batchDeleteMinCutOff') || Twinkle.batchdelete.currentUnlinkCounter <= Twinkle.getPref('batchDeleteMinCutOff') ) ) {
Twinkle.batchdelete.unlinkCache = []; // Clear the cache
var pages = work.shift();
Twinkle.batchdelete.currentDeleteCounter += pages.length;
Twinkle.batchdelete.currentUnlinkCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var page = pages[i];
var query = {
'action': 'query',
'titles': page
};
var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchdelete.callbacks.main );
wikipedia_api.params = { page:page, reason:reason, unlink_page:unlink_page, delete_page:delete_page, delete_redirects:delete_redirects };
wikipedia_api.post();
}
}
}
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchdeleteChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchdelete.currentdeletor = window.setInterval( toCall, 1000, work );
};
Twinkle.batchdelete.callbacks = {
main: function( self ) {
var xmlDoc = self.responseXML;
var normal = xmlDoc.evaluate( '//normalized/n/@to', xmlDoc, null, XPathResult.STRING_TYPE, null ).stringValue;
if( normal ) {
self.params.page = normal;
}
var exists = xmlDoc.evaluate( 'boolean(//pages/page[not(@missing)])', xmlDoc, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue;
if( ! exists ) {
self.statelem.error( "It seems that the page doesn't exist, perhaps it has already been deleted" );
return;
}
var query, wikipedia_api;
if( self.params.unlink_page ) {
query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'nonredirects',
'blnamespace': [0, 100], // main space and portal space only
'bltitle': self.params.page,
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops
};
wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain );
wikipedia_api.params = self.params;
wikipedia_api.post();
} else {
--Twinkle.batchdelete.currentUnlinkCounter;
}
if( self.params.delete_page ) {
if (self.params.delete_redirects)
{
query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'redirects',
'bltitle': self.params.page,
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops
};
wikipedia_api = new Morebits.wiki.api( 'Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain );
wikipedia_api.params = self.params;
wikipedia_api.post();
}
var wikipedia_page = new Morebits.wiki.page( self.params.page, 'Deleting page ' + self.params.page );
wikipedia_page.setEditSummary(self.params.reason + Twinkle.getPref('deletionSummaryAd'));
wikipedia_page.deletePage(function( apiobj ) {
--Twinkle.batchdelete.currentDeleteCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl(self.params.page) );
link.setAttribute( 'title', self.params.page );
link.appendChild( document.createTextNode( self.params.page ) );
apiobj.statelem.info( [ 'completed (' , link , ')' ] );
} );
} else {
--Twinkle.batchdelete.currentDeleteCounter;
}
},
deleteRedirectsMain: function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
var total = snapshot.snapshotLength;
if( snapshot.snapshotLength === 0 ) {
return;
}
var statusIndicator = new Morebits.status('Deleting redirects for ' + self.params.page, '0%');
var onsuccess = function( self ) {
var obj = self.params.obj;
var total = self.params.total;
var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%';
obj.update( now );
self.statelem.unlink();
if( self.params.current >= total ) {
obj.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
if( snapshot.snapshotLength === 0 ) {
statusIndicator.info( '100% (completed)' );
Morebits.wiki.removeCheckpoint();
return;
}
var params = $.extend({}, self.params);
params.current = 0;
params.total = total;
params.obj = statusIndicator;
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var title = snapshot.snapshotItem(i).value;
var wikipedia_page = new Morebits.wiki.page( title, "Deleting " + title );
wikipedia_page.setEditSummary('[[WP:QD#G8|G8]]: Redirect to deleted page "' + self.params.page + '"' + Twinkle.getPref('deletionSummaryAd'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.deletePage(onsuccess);
}
},
unlinkBacklinksMain: function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
if( snapshot.snapshotLength === 0 ) {
--Twinkle.batchdelete.currentUnlinkCounter;
return;
}
var statusIndicator = new Morebits.status('Unlinking backlinks to ' + self.params.page, '0%');
var total = snapshot.snapshotLength * 2;
var onsuccess = function( self ) {
var obj = self.params.obj;
var total = self.params.total;
var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%';
obj.update( now );
self.statelem.unlink();
if( self.params.current >= total ) {
obj.info( now + ' (completed)' );
--Twinkle.batchdelete.currentUnlinkCounter;
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
if( snapshot.snapshotLength === 0 ) {
statusIndicator.info( '100% (completed)' );
--Twinkle.batchdelete.currentUnlinkCounter;
Morebits.wiki.removeCheckpoint();
return;
}
self.params.total = total;
self.params.obj = statusIndicator;
self.params.current = 0;
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var title = snapshot.snapshotItem(i).value;
var wikipedia_page = new Morebits.wiki.page( title, "Unlinking on " + title );
var params = $.extend( {}, self.params );
params.title = title;
params.onsuccess = onsuccess;
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.batchdelete.callbacks.unlinkBacklinks);
}
},
unlinkBacklinks: function( pageobj ) {
var params = pageobj.getCallbackParameters();
if( ! pageobj.exists() ) {
// we probably just deleted it, as a recursive backlink
params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } );
Morebits.wiki.actionCompleted();
return;
}
var text;
if( params.title in Twinkle.batchdelete.unlinkCache ) {
text = Twinkle.batchdelete.unlinkCache[ params.title ];
} else {
text = pageobj.getPageText();
}
var old_text = text;
var wikiPage = new Morebits.wikitext.page( text );
wikiPage.removeLink( params.page );
text = wikiPage.getText();
Twinkle.batchdelete.unlinkCache[ params.title ] = text;
if( text === old_text ) {
// Nothing to do, return
params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } );
Morebits.wiki.actionCompleted();
return;
}
pageobj.setEditSummary('Removing link(s) to deleted page ' + self.params.page + Twinkle.getPref('deletionSummaryAd'));
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.save(params.onsuccess);
}
};
/*
****************************************
*** twinklebatchprotect.js: Batch protect module (sysops only)
****************************************
* Mode of invocation: Tab ("P-batch")
* Active on: Existing project pages and user pages; existing and
* non-existing categories; Special:PrefixIndex
* Config directives in: TwinkleConfig
*/
Twinkle.batchprotect = function twinklebatchprotect() {
if( Morebits.userIsInGroup( 'sysop' ) && ((mw.config.get( 'wgArticleId' ) > 0 && (mw.config.get( 'wgNamespaceNumber' ) === 2 ||
mw.config.get( 'wgNamespaceNumber' ) === 4)) || mw.config.get( 'wgNamespaceNumber' ) === 14 ||
mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) {
twAddPortletLink( Twinkle.batchprotect.callback, "P-batch", "tw-pbatch", "Protect pages linked from this page" );
}
};
Twinkle.batchprotect.unlinkCache = {};
Twinkle.batchprotect.callback = function twinklebatchprotectCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Batch protection" );
Window.setScriptName( "Twinkle" );
//Window.addFooterLink( "Protection templates", "Template:Protection templates" );
Window.addFooterLink( "Protection policy", "WP:PROT" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#protect" );
var form = new Morebits.quickForm( Twinkle.batchprotect.callback.evaluate );
form.append({
type: 'checkbox',
name: 'editmodify',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: 'Modify edit protection',
value: 'editmodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
var editlevel = form.append({
type: 'select',
name: 'editlevel',
label: 'Edit protection:',
event: Twinkle.protect.formevents.editlevel
});
editlevel.append({
type: 'option',
label: 'All',
value: 'all'
});
editlevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
editlevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'editexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', selected: true, value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', value:'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append({
type: 'checkbox',
name: 'movemodify',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: 'Modify move protection',
value: 'movemodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
var movelevel = form.append({
type: 'select',
name: 'movelevel',
label: 'Move protection:',
event: Twinkle.protect.formevents.movelevel
});
movelevel.append({
type: 'option',
label: 'All',
value: 'all'
});
movelevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
movelevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'moveexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', selected: true, value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', value:'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append({
type: 'checkbox',
name: 'createmodify',
event: function twinklebatchprotectFormCreatemodifyEvent(e) {
e.target.form.createlevel.disabled = !e.target.checked;
e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all');
e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = (e.target.checked ? "" : "transparent");
},
list: [
{
label: 'Modify create protection',
value: 'createmodify',
tooltip: 'Only for pages that do not exist.',
checked: true
}
]
});
var createlevel = form.append({
type: 'select',
name: 'createlevel',
label: 'Create protection:',
event: Twinkle.protect.formevents.createlevel
});
createlevel.append({
type: 'option',
label: 'All',
value: 'all'
});
createlevel.append({
type: 'option',
label: 'Autoconfirmed',
value: 'autoconfirmed'
});
createlevel.append({
type: 'option',
label: 'Sysop',
value: 'sysop',
selected: true
});
form.append({
type: 'select',
name: 'createexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: [
{ label: '1 hour', value: '1 hour' },
{ label: '2 hours', value: '2 hours' },
{ label: '3 hours', value: '3 hours' },
{ label: '6 hours', value: '6 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '1 day', value: '1 day' },
{ label: '2 days', value: '2 days' },
{ label: '3 days', value: '3 days' },
{ label: '4 days', value: '4 days' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '2 months', value: '2 months' },
{ label: '3 months', value: '3 months' },
{ label: '1 year', value: '1 year' },
{ label: 'indefinite', selected: true, value: 'indefinite' },
{ label: 'Custom...', value: 'custom' }
]
});
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason (for protection log): '
} );
var query;
if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // categories
query = {
'action': 'query',
'generator': 'categorymembers',
'gcmtitle': mw.config.get( 'wgPageName' ),
'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': 'revisions',
'rvprop': 'size'
};
} else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) {
query = {
'action': 'query',
'generator': 'allpages',
'gapnamespace': Morebits.queryString.exists('namespace') ? Morebits.queryString.get( 'namespace' ) : document.getElementById('namespace').value,
'gapprefix': Morebits.queryString.exists('from') ? Morebits.string.toUpperCaseFirstChar(Morebits.queryString.get( 'from' ).replace('+', ' ')) :
Morebits.string.toUpperCaseFirstChar(document.getElementById('nsfrom').value),
'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops
'prop': 'revisions',
'rvprop': 'size'
};
} else {
query = {
'action': 'query',
'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops
'generator': 'links',
'titles': mw.config.get( 'wgPageName' ),
'prop': 'revisions',
'rvprop': 'size'
};
}
var statusdiv = document.createElement("div");
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.status.init(statusdiv);
Window.display();
var statelem = new Morebits.status("Grabbing list of pages");
var wikipedia_api = new Morebits.wiki.api( 'loading...', query, function(apiobj) {
var xml = apiobj.responseXML;
var $pages = $(xml).find('page');
var list = [];
$pages.each(function(index, page) {
var $page = $(page);
var title = $page.attr('title');
var isRedir = $page.attr('redirect') === ""; // XXX ??
var missing = $page.attr('missing') === ""; // XXX ??
var size = $page.find('rev').attr('size');
var metadata = [];
if (missing) {
metadata.push("page does not exist");
} else {
if (isRedir) {
metadata.push("redirect");
}
metadata.push(size + " bytes");
}
list.push( { label: title + (metadata.length ? (' (' + metadata.join('; ') + ')') : '' ), value: title, checked: true });
});
form.append({ type: 'header', label: 'Pages to protect' });
form.append( {
type: 'checkbox',
name: 'pages',
list: list
} );
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
}, statelem );
wikipedia_api.post();
};
Twinkle.batchprotect.currentProtectCounter = 0;
Twinkle.batchprotect.currentprotector = 0;
Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) {
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
var editmodify = event.target.editmodify.checked;
var editlevel = event.target.editlevel.value;
var editexpiry = event.target.editexpiry.value;
var movemodify = event.target.movemodify.checked;
var movelevel = event.target.movelevel.value;
var moveexpiry = event.target.moveexpiry.value;
var createmodify = event.target.createmodify.checked;
var createlevel = event.target.createlevel.value;
var createexpiry = event.target.createexpiry.value;
if( ! reason ) {
alert("You've got to give a reason, you rouge admin!");
return;
}
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'Nothing to protect, aborting' );
return;
}
var toCall = function twinklebatchprotectToCall( work ) {
if( work.length === 0 && Twinkle.batchprotect.currentProtectCounter <= 0 ) {
Morebits.status.info( 'work done' );
window.clearInterval( Twinkle.batchprotect.currentprotector );
Twinkle.batchprotect.currentprotector = Twinkle.batchprotect.currentProtectCounter = 0;
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && Twinkle.batchprotect.currentProtectCounter <= Twinkle.getPref('batchProtectMinCutOff') ) {
var pages = work.shift();
Twinkle.batchprotect.currentProtectCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var page = pages[i];
var query = {
'action': 'query',
'titles': page
};
var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchprotect.callbacks.main );
wikipedia_api.params = {
page: page,
reason: reason,
editmodify: editmodify,
editlevel: editlevel,
editexpiry: editexpiry,
movemodify: movemodify,
movelevel: movelevel,
moveexpiry: moveexpiry,
createmodify: createmodify,
createlevel: createlevel,
createexpiry: createexpiry
};
wikipedia_api.post();
}
}
};
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchProtectChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchprotect.currentprotector = window.setInterval( toCall, 1000, work );
};
Twinkle.batchprotect.callbacks = {
main: function( apiobj ) {
var xml = apiobj.responseXML;
var normal = $(xml).find('normalized n').attr('to');
if( normal ) {
apiobj.params.page = normal;
}
var exists = ($(xml).find('page').attr('missing') !== "");
var page = new Morebits.wiki.page(apiobj.params.page, "Protecting " + apiobj.params.page);
var takenAction = false;
if (exists && apiobj.params.editmodify) {
page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry);
takenAction = true;
}
if (exists && apiobj.params.movemodify) {
page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry);
takenAction = true;
}
if (!exists && apiobj.params.createmodify) {
page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry);
takenAction = true;
}
if (!takenAction) {
Morebits.status.warn("Protecting " + apiobj.params.page, "page " + (exists ? "exists" : "does not exist") + "; nothing to do, skipping");
return;
}
page.setEditSummary(apiobj.params.reason);
page.protect(function(pageobj) {
--Twinkle.batchprotect.currentProtectCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl( apiobj.params.page ) );
link.appendChild( document.createTextNode( apiobj.params.page ) );
pageobj.getStatusElement().info( [ 'completed (' , link , ')' ] );
} );
}
};
/*
****************************************
*** twinklebatchundelete.js: Batch undelete module
****************************************
* Mode of invocation: Tab ("Und-batch")
* Active on: Existing and non-existing user pages (??? why?)
* Config directives in: TwinkleConfig
*/
// XXX TODO this module needs to be overhauled to use Morebits.wiki.page
Twinkle.batchundelete = function twinklebatchundelete() {
if( mw.config.get("wgNamespaceNumber") !== mw.config.get("wgNamespaceIds").user ) {
return;
}
if( Morebits.userIsInGroup( 'sysop' ) ) {
twAddPortletLink( Twinkle.batchundelete.callback, "Und-batch", "tw-batch-undel", "Undelete 'em all" );
}
};
Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setScriptName("Twinkle");
Window.setTitle("Batch undelete")
var form = new Morebits.quickForm( Twinkle.batchundelete.callback.evaluate );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: '
} );
var query = {
'action': 'query',
'generator': 'links',
'titles': mw.config.get("wgPageName"),
'gpllimit' : Twinkle.getPref('batchMax') // the max for sysops
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) {
var xmlDoc = self.responseXML;
var snapshot = xmlDoc.evaluate('//page[@missing]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null );
var list = [];
for ( var i = 0; i < snapshot.snapshotLength; ++i ) {
var object = snapshot.snapshotItem(i);
var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue;
list.push( {label:page, value:page, checked: true });
}
self.params.form.append( {
type: 'checkbox',
name: 'pages',
list: list
}
);
self.params.form.append( { type:'submit' } );
var result = self.params.form.render();
self.params.Window.setContent( result );
} );
wikipedia_api.params = { form:form, Window:Window };
wikipedia_api.post();
var root = document.createElement( 'div' );
Morebits.status.init( root );
Window.setContent( root );
Window.display();
};
Twinkle.batchundelete.currentUndeleteCounter = 0;
Twinkle.batchundelete.currentundeletor = 0;
Twinkle.batchundelete.callback.evaluate = function( event ) {
Morebits.wiki.actionCompleted.notice = 'Status';
Morebits.wiki.actionCompleted.postfix = 'batch undeletion is now completed';
var pages = event.target.getChecked( 'pages' );
var reason = event.target.reason.value;
if( ! reason ) {
alert("You need to give a reason, you cabal crony!");
return;
}
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init( event.target );
if( !pages ) {
Morebits.status.error( 'Error', 'nothing to undelete, aborting' );
return;
}
var work = Morebits.array.chunk( pages, Twinkle.getPref('batchUndeleteChunks') );
Morebits.wiki.addCheckpoint();
Twinkle.batchundelete.currentundeletor = window.setInterval( Twinkle.batchundelete.callbacks.main, 1000, work, reason );
};
Twinkle.batchundelete.callbacks = {
main: function( work, reason ) {
if( work.length === 0 && Twinkle.batchundelete.currentUndeleteCounter <= 0 ) {
Morebits.status.info( 'work done' );
window.clearInterval( Twinkle.batchundelete.currentundeletor );
Morebits.wiki.removeCheckpoint();
return;
} else if( work.length !== 0 && Twinkle.batchundelete.currentUndeleteCounter <= Twinkle.getPref('batchUndeleteMinCutOff') ) {
var pages = work.shift();
Twinkle.batchundelete.currentUndeleteCounter += pages.length;
for( var i = 0; i < pages.length; ++i ) {
var title = pages[i];
var query = {
'token': mw.user.tokens.get().editToken,
'title': title,
'action': 'undelete',
'reason': reason + Twinkle.getPref('deletionSummaryAd')
};
var wikipedia_api = new Morebits.wiki.api( "Undeleting " + title, query, function( self ) {
--Twinkle.batchundelete.currentUndeleteCounter;
var link = document.createElement( 'a' );
link.setAttribute( 'href', mw.util.getUrl(self.itsTitle) );
link.setAttribute( 'title', self.itsTitle );
link.appendChild( document.createTextNode(self.itsTitle) );
self.statelem.info( ['completed (',link,')'] );
});
wikipedia_api.itsTitle = title;
wikipedia_api.post();
}
}
}
};
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences and user
subpages named "/Twinkle preferences", and adds ad box to the top of user
subpages belonging to the currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
* Config directives in: TwinkleConfig
I, [[User:This, that and the other]], originally wrote this. If the code is misbehaving, or you have any
questions, don't hesitate to ask me. (This doesn't at all imply [[WP:OWN]]ership - it's just meant to
point you in the right direction.) -- TTO
*/
Twinkle.config = {};
Twinkle.config.commonEnums = {
watchlist: { yes: "Add to watchlist", no: "Don't add to watchlist", "default": "Follow your site preferences" },
talkPageMode: { window: "In a window, replacing other user talks", tab: "In a new tab", blank: "In a totally new window" }
};
Twinkle.config.commonSets = {
csdCriteria: {
db: "Custom rationale"
},
csdCriteriaDisplayOrder: [
"db"
],
csdCriteriaNotificationDisplayOrder: [
"db"
],
csdAndDICriteria: {
db: "Custom rationale"
},
csdAndDICriteriaDisplayOrder: [
"db"
],
namespacesNoSpecial: {
"0": "Article",
"1": "Talk (article)",
"2": "User",
"3": "User talk",
"4": "Wikipedia",
"5": "Wikipedia talk",
"6": "File",
"7": "File talk",
"8": "MediaWiki",
"9": "MediaWiki talk",
"10": "Template",
"11": "Template talk",
"12": "Help",
"13": "Help talk",
"14": "Category",
"15": "Category talk"
}
};
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* inFriendlyConfig: <true for preferences located under FriendlyConfig rather than TwinkleConfig>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: "General",
preferences: [
// TwinkleConfig.summaryAd (string)
// Text to be appended to the edit summary of edits made using Twinkle
{
name: "summaryAd",
label: "\"Ad\" to be appended to Twinkle's edit summaries",
helptip: "The summary ad should start with a space, and be kept short.",
type: "string"
},
// TwinkleConfig.deletionSummaryAd (string)
// Text to be appended to the edit summary of deletions made using Twinkle
{
name: "deletionSummaryAd",
label: "Summary ad to use for deletion summaries",
helptip: "Normally the same as the edit summary ad above.",
adminOnly: true,
type: "string"
},
// TwinkleConfig.protectionSummaryAd (string)
// Text to be appended to the edit summary of page protections made using Twinkle
{
name: "protectionSummaryAd",
label: "Summary ad to use for page protections",
helptip: "Normally the same as the edit summary ad above.",
adminOnly: true,
type: "string"
},
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: "userTalkPageMode",
label: "When opening a user talk page, open it",
type: "enum",
enumValues: Twinkle.config.commonEnums.talkPageMode
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: "dialogLargeFont",
label: "Use larger text in Twinkle dialogs",
type: "boolean"
}
]
},
{
title: "Revert and rollback", // twinklefluff module
preferences: [
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: "openTalkPage",
label: "Open user talk page after these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contrib page, because from there, actions may be multiple, and opening talk page not suitable. If set to true, openTalkPage defines then if talk page will be opened.
{
name: "openTalkPageOnAutoRevert",
label: "Open user talk page when invoking rollback from user contributions",
helptip: "Often, you may be rolling back many pages at a time from a vandal's contributions page, so it would be unsuitable to open the user talk page. Hence, this option is off by default. When this is on, the desired options must be enabled in the previous setting for this to work.",
type: "boolean"
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: "markRevertedPagesAsMinor",
label: "Mark as minor edit for these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: "watchRevertedPages",
label: "Add pages to watchlist for these types of reversions",
type: "set",
setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" }
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: "offerReasonOnNormalRevert",
label: "Prompt for reason for normal rollbacks",
helptip: "\"Normal\" rollbacks are the ones that are invoked from the middle [rollback] link.",
type: "boolean"
},
{
name: "confirmOnFluff",
label: "Provide a confirmation message before reverting",
helptip: "For users of pen or touch devices, and chronically indecisive people.",
type: "boolean"
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links (diff, others, mine, contribs)
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: "showRollbackLinks",
label: "Show rollback links on these pages",
type: "set",
setValues: { diff: "Diff pages", others: "Contributions pages of other users", mine: "My contributions page" }
}
]
},
{
title: "Deletion tagging",
preferences: [
{
name: "speedySelectionStyle",
label: "When to go ahead and tag/delete the page",
type: "enum",
enumValues: { "buttonClick": 'When I click "Submit"', "radioClick": "As soon as I click an option" }
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as patrolled (if the page was reached from NewPages)
{
name: "markSpeedyPagesAsPatrolled",
label: "Mark page as patrolled when tagging (if possible)",
helptip: "Due to technical limitations, pages are only marked as patrolled when they are reached via Special:NewPages.",
type: "boolean"
},
// TwinkleConfig.openUserTalkPageOnSpeedyDelete (array of strings)
// What types of actions that should result user talk page to be opened when speedily deleting (admin only)
{
name: "openUserTalkPageOnSpeedyDelete",
label: "Open user talk page when deleting under these criteria",
adminOnly: true,
type: "set",
setValues: Twinkle.config.commonSets.csdAndDICriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndDICriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: "deleteTalkPageOnDelete",
label: "Check the \"also delete talk page\" box by default",
adminOnly: true,
type: "boolean"
},
// TwinkleConfig.deleteSysopDefaultToTag (boolean)
// Make the CSD screen default to "tag" instead of "delete" (admin only)
{
name: "deleteSysopDefaultToTag",
label: "Default to tagging instead of outright deletion",
adminOnly: true,
type: "boolean"
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: "speedyWindowWidth",
label: "Width of deletion window (pixels)",
type: "integer"
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: "speedyWindowHeight",
label: "Height of deletion window (pixels)",
helptip: "If you have a big monitor, you might like to increase this.",
type: "integer"
},
{
name: "logSpeedyNominations",
label: "Keep a log in userspace of all deletion nominations",
helptip: "Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you nominate for QD using Twinkle. Files tagged using DI are also added to this log.",
type: "boolean"
},
{
name: "speedyLogPageName",
label: "Keep the deletion userspace log at this user subpage",
helptip: "i.e. User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the deletion userspace log.",
type: "string"
}
]
},
{
title: "Unlink",
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article) and 100 (portal)
{
name: "unlinkNamespaces",
label: "Remove links from pages in these namespaces",
helptip: "Avoid selecting any talk namespaces, as Twinkle might end up unlinking on talk archives (a big no-no).",
type: "set",
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: "Hidden",
hidden: true,
preferences: [
// twinkle.header.js: portlet setup
{
name: "portletArea",
type: "string"
},
{
name: "portletId",
type: "string"
},
{
name: "portletName",
type: "string"
},
{
name: "portletType",
type: "string"
},
{
name: "portletNext",
type: "string"
},
// twinklefluff.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: "revertMaxRevisions",
type: "integer"
},
// twinklebatchdelete.js: How many pages should be processed at a time
{
name: "batchdeleteChunks",
type: "integer"
},
// twinklebatchdelete.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchDeleteMinCutOff",
type: "integer"
},
// twinklebatchdelete.js: How many pages should be processed maximum
{
name: "batchMax",
type: "integer"
},
// twinklebatchprotect.js: How many pages should be processed at a time
{
name: "batchProtectChunks",
type: "integer"
},
// twinklebatchprotect.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchProtectMinCutOff",
type: "integer"
},
// twinklebatchundelete.js: How many pages should be processed at a time
{
name: "batchundeleteChunks",
type: "integer"
},
// twinklebatchundelete.js: How many pages left in the process of being completed should allow a new batch to be initialized
{
name: "batchUndeleteMinCutOff",
type: "integer"
}
]
}
]; // end of Twinkle.config.sections
//{
// name: "",
// label: "",
// type: ""
// },
Twinkle.config.init = function twinkleconfigInit() {
if (( mw.config.get("wgTitle") === "Twinkle/Preferences" ||
(mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user && mw.config.get("wgTitle").lastIndexOf("/Twinkle preferences") === (mw.config.get("wgTitle").length - 20))) &&
mw.config.get("wgAction") === "view") {
// create the config page at Wikipedia:Twinkle/Preferences, and at user subpages (for testing purposes)
if (!document.getElementById("twinkle-config")) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style (the url() CSS function doesn't seem to work from wikicode - ?!)
document.getElementById("twinkle-config-titlebar").style.backgroundImage = "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC)";
var contentdiv = document.getElementById("twinkle-config-content");
contentdiv.textContent = ""; // clear children
// let user know about possible conflict with monobook.js/vector.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
var contentnotice = document.createElement("p");
// I hate innerHTML, but this is one thing it *is* good for...
contentnotice.innerHTML = "<b>Before modifying your preferences here,</b> make sure you have removed any old <code>TwinkleConfig</code> and <code>FriendlyConfig</code> settings from your <a href=\"" + mw.util.getUrl("Special:MyPage/skin.js") + "\" title=\"Special:MyPage/skin.js\">user JavaScript file</a>.";
contentdiv.appendChild(contentnotice);
// look and see if the user does in fact have any old settings in their skin JS file
var skinjs = new Morebits.wiki.page("User:" + mw.config.get("wgUserName") + "/" + mw.config.get("skin") + ".js");
skinjs.setCallbackParameters(contentnotice);
skinjs.load(Twinkle.config.legacyPrefsNotice);
// start a table of contents
var toctable = document.createElement("table");
toctable.className = "toc";
toctable.style.marginLeft = "0.4em";
var toctr = document.createElement("tr");
var toctd = document.createElement("td");
// create TOC title
var toctitle = document.createElement("div");
toctitle.id = "toctitle";
var toch2 = document.createElement("h2");
toch2.textContent = "Contents ";
toctitle.appendChild(toch2);
// add TOC show/hide link
var toctoggle = document.createElement("span");
toctoggle.className = "toctoggle";
toctoggle.appendChild(document.createTextNode("["));
var toctogglelink = document.createElement("a");
toctogglelink.className = "internal";
toctogglelink.setAttribute("href", "#tw-tocshowhide");
toctogglelink.textContent = "hide";
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode("]"));
toctitle.appendChild(toctoggle);
toctd.appendChild(toctitle);
// create item container: this is what we add stuff to
var tocul = document.createElement("ul");
toctogglelink.addEventListener("click", function twinkleconfigTocToggle() {
var $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(":visible").length) {
toctogglelink.textContent = "hide";
} else {
toctogglelink.textContent = "show";
}
}, false);
toctd.appendChild(tocul);
toctr.appendChild(toctd);
toctable.appendChild(toctr);
contentdiv.appendChild(toctable);
var tocnumber = 1;
var contentform = document.createElement("form");
contentform.setAttribute("action", "javascript:void(0)"); // was #tw-save - changed to void(0) to work around Chrome issue
contentform.addEventListener("submit", Twinkle.config.save, true);
contentdiv.appendChild(contentform);
var container = document.createElement("table");
container.style.width = "100%";
contentform.appendChild(container);
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // i.e. "continue" in this context
}
var configgetter; // retrieve the live config values
if (section.inFriendlyConfig) {
configgetter = Twinkle.getFriendlyPref;
} else {
configgetter = Twinkle.getPref;
}
// add to TOC
var tocli = document.createElement("li");
tocli.className = "toclevel-1";
var toca = document.createElement("a");
toca.setAttribute("href", "#twinkle-config-section-" + tocnumber.toString());
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
var row = document.createElement("tr");
var cell = document.createElement("td");
cell.setAttribute("colspan", "3");
var heading = document.createElement("h4");
heading.style.borderBottom = "1px solid gray";
heading.style.marginTop = "0.2em";
heading.id = "twinkle-config-section-" + (tocnumber++).toString();
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
var rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each(function(prefkey, pref) {
if (pref.adminOnly && !Morebits.userIsInGroup("sysop")) {
return true; // i.e. "continue" in this context
}
row = document.createElement("tr");
row.style.marginBottom = "0.2em";
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = "rgba(128, 128, 128, 0.1)";
}
cell = document.createElement("td");
var label, input;
switch (pref.type) {
case "boolean": // create a checkbox
cell.setAttribute("colspan", "2");
label = document.createElement("label");
input = document.createElement("input");
input.setAttribute("type", "checkbox");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
if (configgetter(pref.name) === true) {
input.setAttribute("checked", "checked");
}
label.appendChild(input);
label.appendChild(document.createTextNode(" " + pref.label));
cell.appendChild(label);
break;
case "string": // create an input box
case "integer":
// add label to first column
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
if (pref.type === "integer") {
input.setAttribute("size", 6);
input.setAttribute("type", "number");
input.setAttribute("step", "1"); // integers only
}
if (configgetter(pref.name)) {
input.setAttribute("value", configgetter(pref.name));
}
cell.appendChild(input);
break;
case "enum": // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
input = document.createElement("select");
input.setAttribute("id", pref.name);
input.setAttribute("name", pref.name);
$.each(pref.enumValues, function(enumvalue, enumdisplay) {
var option = document.createElement("option");
option.setAttribute("value", enumvalue);
if (configgetter(pref.name) === enumvalue) {
option.setAttribute("selected", "selected");
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case "set": // create a set of check boxes
// add label first of all
cell.setAttribute("colspan", "2");
label = document.createElement("label"); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
var checkdiv = document.createElement("div");
checkdiv.style.paddingLeft = "1em";
var worker = function(itemkey, itemvalue) {
var checklabel = document.createElement("label");
checklabel.style.marginRight = "0.7em";
checklabel.style.display = "inline-block";
var check = document.createElement("input");
check.setAttribute("type", "checkbox");
check.setAttribute("id", pref.name + "_" + itemkey);
check.setAttribute("name", pref.name + "_" + itemkey);
if (configgetter(pref.name) && configgetter(pref.name).indexOf(itemkey) !== -1) {
check.setAttribute("checked", "checked");
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === "unlinkNamespaces") {
if (configgetter(pref.name) && configgetter(pref.name).indexOf(parseInt(itemkey, 10)) !== -1) {
check.setAttribute("checked", "checked");
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, function(itemkey, item) {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case "customList":
// add label to first column
cell.style.textAlign = "right";
cell.style.paddingRight = "0.5em";
label = document.createElement("label");
label.setAttribute("for", pref.name);
label.appendChild(document.createTextNode(pref.label + ":"));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement("td");
cell.style.paddingRight = "1em";
var button = document.createElement("button");
button.setAttribute("id", pref.name);
button.setAttribute("name", pref.name);
button.setAttribute("type", "button");
button.addEventListener("click", Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: configgetter(pref.name),
pref: pref,
inFriendlyConfig: section.inFriendlyConfig
});
button.appendChild(document.createTextNode("Edit items"));
cell.appendChild(button);
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement("td");
cell.style.fontSize = "90%";
cell.style.color = "gray";
if (pref.helptip) {
cell.innerHTML = pref.helptip;
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== "customList") {
var resetlink = document.createElement("a");
resetlink.setAttribute("href", "#tw-reset");
resetlink.setAttribute("id", "twinkle-config-reset-" + pref.name);
resetlink.addEventListener("click", Twinkle.config.resetPrefLink, false);
if (resetlink.style.styleFloat) { // IE (inc. IE9)
resetlink.style.styleFloat = "right";
} else { // standards
resetlink.style.cssFloat = "right";
}
resetlink.style.margin = "0 0.6em";
resetlink.appendChild(document.createTextNode("Reset"));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
var footerbox = document.createElement("div");
footerbox.setAttribute("id", "twinkle-config-buttonpane");
footerbox.style.backgroundColor = "#BCCADF";
footerbox.style.padding = "0.5em";
var button = document.createElement("button");
button.setAttribute("id", "twinkle-config-submit");
button.setAttribute("type", "submit");
button.appendChild(document.createTextNode("Save changes"));
footerbox.appendChild(button);
var footerspan = document.createElement("span");
footerspan.className = "plainlinks";
footerspan.style.marginLeft = "2.4em";
footerspan.style.fontSize = "90%";
var footera = document.createElement("a");
footera.setAttribute("href", "#tw-reset-all");
footera.setAttribute("id", "twinkle-config-resetall");
footera.addEventListener("click", Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode("Restore defaults"));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (location.hash) {
location.hash = location.hash;
}
} else if (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user) {
var box = document.createElement("div");
box.setAttribute("id", "twinkle-config-headerbox");
box.style.border = "1px #f60 solid";
box.style.background = "#fed";
box.style.padding = "0.6em";
box.style.margin = "0.5em auto";
box.style.textAlign = "center";
var link;
if (mw.config.get("wgTitle") === mw.config.get("wgUserName") + "/twinkleoptions.js") {
// place "why not try the preference panel" notice
box.style.fontWeight = "bold";
box.style.width = "80%";
box.style.borderWidth = "2px";
if (mw.config.get("wgArticleId") > 0) { // page exists
box.appendChild(document.createTextNode("This page contains your Twinkle preferences. You can change them using the "));
} else { // page does not exist
box.appendChild(document.createTextNode("You can customize Twinkle to suit your preferences by using the "));
}
link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") );
link.appendChild(document.createTextNode("Twinkle preferences panel"));
box.appendChild(link);
box.appendChild(document.createTextNode(", or by editing this page."));
$(box).insertAfter($("#contentSub"));
} else if (mw.config.get("wgTitle").indexOf(mw.config.get("wgUserName")) === 0 &&
mw.config.get("wgPageName").lastIndexOf(".js") === mw.config.get("wgPageName").length - 3) {
// place "Looking for Twinkle options?" notice
box.style.width = "60%";
box.appendChild(document.createTextNode("If you want to set Twinkle preferences, you can use the "));
link = document.createElement("a");
link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") );
link.appendChild(document.createTextNode("Twinkle preferences panel"));
box.appendChild(link);
box.appendChild(document.createTextNode("."));
$(box).insertAfter($("#contentSub"));
}
}
};
// Morebits.wiki.page callback from init code
Twinkle.config.legacyPrefsNotice = function twinkleconfigLegacyPrefsNotice(pageobj) {
var text = pageobj.getPageText();
var contentnotice = pageobj.getCallbackParameters();
if (text.indexOf("TwinkleConfig") !== -1 || text.indexOf("FriendlyConfig") !== -1) {
contentnotice.innerHTML = '<table class="plainlinks ombox ombox-content"><tr><td class="mbox-image">' +
'<img alt="" src="http://upload.wikimedia.org/wikipedia/en/3/38/Imbox_content.png" /></td>' +
'<td class="mbox-text"><p><big><b>Before modifying your settings here,</b> you must remove your old Twinkle and Friendly settings from your personal skin JavaScript.</big></p>' +
'<p>To do this, you can <a href="' + mw.config.get("wgScript") + '?title=User:' + encodeURIComponent(mw.config.get("wgUserName")) + '/' + mw.config.get("skin") + '.js&action=edit" target="_tab"><b>edit your personal JavaScript</b></a>, removing all lines of code that refer to <code>TwinkleConfig</code> and <code>FriendlyConfig</code>.</p>' +
'</td></tr></table>';
} else {
$(contentnotice).remove();
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow(dlgtable, value, label) {
var contenttr = document.createElement("tr");
// "remove" button
var contenttd = document.createElement("td");
var removeButton = document.createElement("button");
removeButton.setAttribute("type", "button");
removeButton.addEventListener("click", function() { $(contenttr).remove(); }, false);
removeButton.textContent = "Remove";
contenttd.appendChild(removeButton);
contenttr.appendChild(contenttd);
// value input box
contenttd = document.createElement("td");
var input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "twinkle-config-customlist-value";
input.style.width = "97%";
if (value) {
input.setAttribute("value", value);
}
contenttd.appendChild(input);
contenttr.appendChild(contenttd);
// label input box
contenttd = document.createElement("td");
input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "twinkle-config-customlist-label";
input.style.width = "98%";
if (label) {
input.setAttribute("value", label);
}
contenttd.appendChild(input);
contenttr.appendChild(contenttd);
dlgtable.appendChild(contenttr);
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
var $prefbutton = $(e.target);
var curvalue = $prefbutton.data("value");
var curpref = $prefbutton.data("pref");
var dialog = new Morebits.simpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName("Twinkle preferences");
var dialogcontent = document.createElement("div");
var dlgtable = document.createElement("table");
dlgtable.className = "wikitable";
dlgtable.style.margin = "1.4em 1em";
dlgtable.style.width = "auto";
var dlgtbody = document.createElement("tbody");
// header row
var dlgtr = document.createElement("tr");
// top-left cell
var dlgth = document.createElement("th");
dlgth.style.width = "5%";
dlgtr.appendChild(dlgth);
// value column header
dlgth = document.createElement("th");
dlgth.style.width = "35%";
dlgth.textContent = (curpref.customListValueTitle ? curpref.customListValueTitle : "Value");
dlgtr.appendChild(dlgth);
// label column header
dlgth = document.createElement("th");
dlgth.style.width = "60%";
dlgth.textContent = (curpref.customListLabelTitle ? curpref.customListLabelTitle : "Label");
dlgtr.appendChild(dlgth);
dlgtbody.appendChild(dlgtr);
// content rows
var gotRow = false;
$.each(curvalue, function(k, v) {
gotRow = true;
Twinkle.config.listDialog.addRow(dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow(dlgtbody);
}
// final "add" button
var dlgtfoot = document.createElement("tfoot");
dlgtr = document.createElement("tr");
var dlgtd = document.createElement("td");
dlgtd.setAttribute("colspan", "3");
var addButton = document.createElement("button");
addButton.style.minWidth = "8em";
addButton.setAttribute("type", "button");
addButton.addEventListener("click", function(e) {
Twinkle.config.listDialog.addRow(dlgtbody);
}, false);
addButton.textContent = "Add";
dlgtd.appendChild(addButton);
dlgtr.appendChild(dlgtd);
dlgtfoot.appendChild(dlgtr);
dlgtable.appendChild(dlgtbody);
dlgtable.appendChild(dlgtfoot);
dialogcontent.appendChild(dlgtable);
// buttonpane buttons: [Save changes] [Reset] [Cancel]
var button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
Twinkle.config.listDialog.save($prefbutton, dlgtbody);
dialog.close();
}, false);
button.textContent = "Save changes";
dialogcontent.appendChild(button);
button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
Twinkle.config.listDialog.reset($prefbutton, dlgtbody);
}, false);
button.textContent = "Reset";
dialogcontent.appendChild(button);
button = document.createElement("button");
button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane
button.addEventListener("click", function(e) {
dialog.close(); // the event parameter on this function seems to be broken
}, false);
button.textContent = "Cancel";
dialogcontent.appendChild(button);
dialog.setContent(dialogcontent);
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset(button, tbody) {
// reset value on button
var $button = $(button);
var curpref = $button.data("pref");
var oldvalue = $button.data("value");
Twinkle.config.resetPref(curpref, $button.data("inFriendlyConfig"));
// reset form
var $tbody = $(tbody);
$tbody.find("tr").slice(1).remove(); // all rows except the first (header) row
// add the new values
var curvalue = $button.data("value");
$.each(curvalue, function(k, v) {
Twinkle.config.listDialog.addRow(tbody, v.value, v.label);
});
// save the old value
$button.data("value", oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave(button, tbody) {
var result = [];
var current = {};
$(tbody).find('input[type="text"]').each(function(inputkey, input) {
if ($(input).hasClass("twinkle-config-customlist-value")) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$(button).data("value", result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
var wantedpref = e.target.id.substring(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // continue: skip impossibilities
}
var foundit = false;
$(section.preferences).each(function(prefkey, pref) {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref, section.inFriendlyConfig);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref, inFriendlyConfig) {
switch (pref.type) {
case "boolean":
document.getElementById(pref.name).checked = (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]);
break;
case "string":
case "integer":
case "enum":
document.getElementById(pref.name).value = (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]);
break;
case "set":
$.each(pref.setValues, function(itemkey, itemvalue) {
if (document.getElementById(pref.name + "_" + itemkey)) {
document.getElementById(pref.name + "_" + itemkey).checked = ((inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]).indexOf(itemkey) !== -1);
}
});
break;
case "customList":
$(document.getElementById(pref.name)).data("value", (inFriendlyConfig ?
Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]));
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) {
return true; // continue: skip impossibilities
}
$(section.preferences).each(function(prefkey, pref) {
if (!pref.adminOnly || Morebits.userIsInGroup("sysop")) {
Twinkle.config.resetPref(pref, section.inFriendlyConfig);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.status.init( document.getElementById("twinkle-config-content") );
Morebits.wiki.actionCompleted.notice = "Save";
var userjs = mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user] + ":" + mw.config.get("wgUserName") + "/twinkleoptions.js";
var wikipedia_page = new Morebits.wiki.page(userjs, "Saving preferences to " + userjs);
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
var form = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
// this is the object which gets serialized into JSON
var newConfig = {
twinkle: {},
friendly: {}
};
// keeping track of all preferences that we encounter
// any others that are set in the user's current config are kept
// this way, preferences that this script doesn't know about are not lost
// (it does mean obsolete prefs will never go away, but... ah well...)
var foundTwinklePrefs = [], foundFriendlyPrefs = [];
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
var compare = function(a, b) {
if ($.isArray(a)) {
if (a.length !== b.length) {
return false;
}
var asort = a.sort(), bsort = b.sort();
for (var i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === "object") && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
} else {
return a === b;
}
};
$(Twinkle.config.sections).each(function(sectionkey, section) {
if (section.adminOnly && !Morebits.userIsInGroup("sysop")) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each(function(prefkey, pref) {
var userValue; // = undefined
// only read form values for those prefs that have them
if (!section.hidden && (!pref.adminOnly || Morebits.userIsInGroup("sysop"))) {
switch (pref.type) {
case "boolean": // read from the checkbox
userValue = form[pref.name].checked;
break;
case "string": // read from the input box or combo box
case "enum":
userValue = form[pref.name].value;
break;
case "integer": // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.status.warn("Saving", "The value you specified for " + pref.name + " (" + pref.value + ") was invalid. The save will continue, but the invalid data value will be skipped.");
userValue = null;
}
break;
case "set": // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, function(itemkey, item) {
if (form[pref.name + "_" + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, function(itemkey, itemvalue) {
if (form[pref.name + "_" + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case "customList": // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data("value");
break;
default:
alert("twinkleconfig: unknown data type for preference " + pref.name);
break;
}
}
// only save those preferences that are *different* from the default
if (section.inFriendlyConfig) {
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.friendly[pref.name])) {
newConfig.friendly[pref.name] = userValue;
}
foundFriendlyPrefs.push(pref.name);
} else {
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.twinkle[pref.name])) {
newConfig.twinkle[pref.name] = userValue;
}
foundTwinklePrefs.push(pref.name);
}
});
});
if (Twinkle.prefs) {
$.each(Twinkle.prefs.twinkle, function(tkey, tvalue) {
if (foundTwinklePrefs.indexOf(tkey) === -1) {
newConfig.twinkle[tkey] = tvalue;
}
});
$.each(Twinkle.prefs.friendly, function(fkey, fvalue) {
if (foundFriendlyPrefs.indexOf(fkey) === -1) {
newConfig.friendly[fkey] = fvalue;
}
});
}
var text =
"// twinkleoptions.js: personal Twinkle preferences file\n" +
"//\n" +
"// NOTE: The easiest way to change your Twinkle preferences is by using the\n" +
"// Twinkle preferences panel, at [[" + mw.config.get("wgPageName") + "]].\n" +
"//\n" +
"// This file is AUTOMATICALLY GENERATED. Any changes you make (aside from\n" +
"// changing the configuration parameters in a valid-JavaScript way) will be\n" +
"// overwritten the next time you click \"save\" in the Twinkle preferences\n" +
"// panel. If modifying this file, make sure to use correct JavaScript.\n" +
"\n" +
"window.Twinkle.prefs = ";
text += JSON.stringify(newConfig, null, 2);
text +=
";\n" +
"\n" +
"// End of twinkleoptions.js\n";
pageobj.setPageText(text);
pageobj.setEditSummary("Saving Twinkle preferences: automatic edit from [[" + mw.config.get("wgPageName") + "]]");
pageobj.setCreateOption("recreate");
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info("successful");
var noticebox = document.createElement("div");
noticebox.className = "successbox";
noticebox.style.fontSize = "100%";
noticebox.style.marginTop = "2em";
noticebox.innerHTML = "<p><b>Your Twinkle preferences have been saved.</b></p><p>To see the changes, you will need to <b>clear your browser cache entirely</b> (see <a href=\"" + mw.util.getUrl("WP:BYPASS") + "\" title=\"WP:BYPASS\">WP:BYPASS</a> for instructions).</p>";
Morebits.status.root.appendChild(noticebox);
var noticeclear = document.createElement("br");
noticeclear.style.clear = "both";
Morebits.status.root.appendChild(noticeclear);
};
/*
****************************************
*** twinklediff.js: Diff module
****************************************
* Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current")
* Active on: Existing non-special pages
* Config directives in: TwinkleConfig
*/
Twinkle.diff = function twinklediff() {
if( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') ) {
return;
}
var query = {
'title': mw.config.get('wgPageName'),
'diff': 'cur',
'oldid': 'prev'
};
twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Last', 'tw-lastdiff', 'Show most recent diff' );
// Show additional tabs only on diff pages
if (Morebits.queryString.exists('diff')) {
twAddPortletLink(function(){ Twinkle.diff.evaluate(false); }, 'Since', 'tw-since', 'Show difference between last diff and the revision made by previous user' );
twAddPortletLink( function(){ Twinkle.diff.evaluate(true); }, 'Since mine', 'tw-sincemine', 'Show difference between last diff and my last revision' );
var oldid = /oldid=(.+)/.exec($('#mw-diff-ntitle1').find('strong a').first().attr("href"))[1];
query = {
'title': mw.config.get('wgPageName'),
'diff': 'cur',
'oldid' : oldid
};
twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Current', 'tw-curdiff', 'Show difference to current revision' );
}
};
Twinkle.diff.evaluate = function twinklediffEvaluate(me) {
var user;
if( me ) {
user = mw.config.get('wgUserName');
} else {
var node = document.getElementById( 'mw-diff-ntitle2' );
if( ! node ) {
// nothing to do?
return;
}
user = $(node).find('a').first().text();
}
var query = {
'prop': 'revisions',
'action': 'query',
'titles': mw.config.get('wgPageName'),
'rvlimit': 1,
'rvprop': [ 'ids', 'user' ],
'rvstartid': mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one
'rvuser': user
};
Morebits.status.init( document.getElementById('bodyContent') );
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of initial contributor', query, Twinkle.diff.callbacks.main );
wikipedia_api.params = { user: user };
wikipedia_api.post();
};
Twinkle.diff.callbacks = {
main: function( self ) {
var xmlDoc = self.responseXML;
var revid = $(xmlDoc).find('rev').attr('revid');
if( ! revid ) {
self.statelem.error( 'no suitable earlier revision found, or ' + self.params.user + ' is the only contributor. Aborting.' );
return;
}
var query = {
'title': mw.config.get('wgPageName'),
'oldid': revid,
'diff': mw.config.get('wgCurRevisionId')
};
window.location = mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query );
}
};
/*
****************************************
*** twinklefluff.js: Revert/rollback module
****************************************
* Mode of invocation: Links on history, contributions, and diff pages
* Active on: Diff pages, history pages, contributions pages
* Config directives in: TwinkleConfig
*/
/**
Twinklefluff revert and antivandalism utility
*/
Twinkle.fluff = {
auto: function() {
if( parseInt( Morebits.queryString.get('oldid'), 10) !== mw.config.get('wgCurRevisionId') ) {
// not latest revision
alert("Can't rollback, page has changed in the meantime.");
return;
}
var vandal = $("#mw-diff-ntitle2").find("a.mw-userlink").text();
Twinkle.fluff.revert( Morebits.queryString.get( 'twinklerevert' ), vandal, true );
},
normal: function() {
var spanTag = function( color, content ) {
var span = document.createElement( 'span' );
span.style.color = color;
span.appendChild( document.createTextNode( content ) );
return span;
};
if( mw.config.get('wgNamespaceNumber') === -1 && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) {
//Get the username these contributions are for
var lastLogNode = $('#contentSub').find('a[title^="Special:Log"]').last();
if(!lastLogNode) return;
var logMatch = /wiki\/Special:Log\/(.+)$/.exec(
lastLogNode ? lastLogNode.attr("href").replace(/_/g, "%20") : ''
);
if(!logMatch) return;
username = decodeURIComponent(logMatch[1]);
if( Twinkle.getPref('showRollbackLinks').indexOf('contribs') !== -1 ||
( mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').indexOf('others') !== -1 ) ||
( mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').indexOf('mine') !== -1 ) ) {
var list = $("#bodyContent").find("ul li:has(span.mw-uctop)");
var revNode = document.createElement('strong');
var revLink = document.createElement('a');
revLink.appendChild( spanTag( 'Black', '[' ) );
revLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) );
revLink.appendChild( spanTag( 'Black', ']' ) );
revNode.appendChild(revLink);
var revVandNode = document.createElement('strong');
var revVandLink = document.createElement('a');
revVandLink.appendChild( spanTag( 'Black', '[' ) );
revVandLink.appendChild( spanTag( 'Red', 'vandalism' ) );
revVandLink.appendChild( spanTag( 'Black', ']' ) );
revVandNode.appendChild(revVandLink);
list.each(function(key, current) {
var href = $(current).children("a:eq(1)").attr("href");
current.appendChild( document.createTextNode(' ') );
var tmpNode = revNode.cloneNode( true );
tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'norm' } ) );
current.appendChild( tmpNode );
current.appendChild( document.createTextNode(' ') );
tmpNode = revVandNode.cloneNode( true );
tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'vand' } ) );
current.appendChild( tmpNode );
});
}
} else {
if( mw.config.get('wgCanonicalSpecialPageName') === "Undelete" ) {
//You can't rollback deleted pages!
return;
}
var body = document.getElementById('bodyContent');
var firstRev = $("div.firstrevisionheader").length;
if( firstRev ) {
// we have first revision here, nothing to do.
return;
}
var otitle, ntitle;
try {
var otitle1 = document.getElementById('mw-diff-otitle1');
var ntitle1 = document.getElementById('mw-diff-ntitle1');
if (!otitle1 || !ntitle1) {
return;
}
otitle = otitle1.parentNode;
ntitle = ntitle1.parentNode;
} catch( e ) {
// no old, nor new title, nothing to do really, return;
return;
}
var old_rev_url = $("#mw-diff-otitle1").find("strong a").attr("href");
// Lets first add a [edit this revision] link
var query = new Morebits.queryString( old_rev_url.split( '?', 2 )[1] );
var oldrev = query.get('oldid');
var revertToRevision = document.createElement('div');
revertToRevision.setAttribute( 'id', 'tw-revert-to-orevision' );
revertToRevision.style.fontWeight = 'bold';
var revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') );
revertToRevisionLink.href = "#";
$(revertToRevisionLink).click(function(){
Twinkle.fluff.revertToRevision(oldrev);
});
revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) );
revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) );
revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) );
otitle.insertBefore( revertToRevision, otitle.firstChild );
if( document.getElementById('differences-nextlink') ) {
// Not latest revision
curVersion = false;
var new_rev_url = $("#mw-diff-ntitle1").find("strong a").attr("href");
query = new Morebits.queryString( new_rev_url.split( '?', 2 )[1] );
var newrev = query.get('oldid');
revertToRevision = document.createElement('div');
revertToRevision.setAttribute( 'id', 'tw-revert-to-nrevision' );
revertToRevision.style.fontWeight = 'bold';
revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') );
revertToRevisionLink.href = "#";
$(revertToRevisionLink).click(function(){
Twinkle.fluff.revertToRevision(newrev);
});
revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) );
revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) );
revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) );
ntitle.insertBefore( revertToRevision, ntitle.firstChild );
return;
}
if( Twinkle.getPref('showRollbackLinks').indexOf('diff') !== -1 ) {
var vandal = $("#mw-diff-ntitle2").find("a").first().text();
var revertNode = document.createElement('div');
revertNode.setAttribute( 'id', 'tw-revert' );
var agfNode = document.createElement('strong');
var vandNode = document.createElement('strong');
var normNode = document.createElement('strong');
var agfLink = document.createElement('a');
var vandLink = document.createElement('a');
var normLink = document.createElement('a');
agfLink.href = "#";
vandLink.href = "#";
normLink.href = "#";
$(agfLink).click(function(){
Twinkle.fluff.revert('agf', vandal);
});
$(vandLink).click(function(){
Twinkle.fluff.revert('vand', vandal);
});
$(normLink).click(function(){
Twinkle.fluff.revert('norm', vandal);
});
agfLink.appendChild( spanTag( 'Black', '[' ) );
agfLink.appendChild( spanTag( 'DarkOliveGreen', 'rollback (AGF)' ) );
agfLink.appendChild( spanTag( 'Black', ']' ) );
vandLink.appendChild( spanTag( 'Black', '[' ) );
vandLink.appendChild( spanTag( 'Red', 'rollback (VANDAL)' ) );
vandLink.appendChild( spanTag( 'Black', ']' ) );
normLink.appendChild( spanTag( 'Black', '[' ) );
normLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) );
normLink.appendChild( spanTag( 'Black', ']' ) );
agfNode.appendChild(agfLink);
vandNode.appendChild(vandLink);
normNode.appendChild(normLink);
revertNode.appendChild( agfNode );
revertNode.appendChild( document.createTextNode(' || ') );
revertNode.appendChild( normNode );
revertNode.appendChild( document.createTextNode(' || ') );
revertNode.appendChild( vandNode );
ntitle.insertBefore( revertNode, ntitle.firstChild );
}
}
}
};
Twinkle.fluff.revert = function revertPage( type, vandal, autoRevert, rev, page ) {
if (mw.util.isIPv6Address(vandal)) {
vandal = Morebits.sanitizeIPv6(vandal);
}
var pagename = page || mw.config.get('wgPageName');
var revid = rev || mw.config.get('wgCurRevisionId');
Morebits.status.init( document.getElementById('bodyContent') );
var params = {
type: type,
user: vandal,
pagename: pagename,
revid: revid,
autoRevert: !!autoRevert
};
var query = {
'action': 'query',
'prop': ['info', 'revisions'],
'titles': pagename,
'rvlimit': 50, // max possible
'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ],
'curtimestamp': '',
'meta': 'tokens',
'type': 'csrf',
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of earlier revisions', query, Twinkle.fluff.callbacks.main );
wikipedia_api.params = params;
wikipedia_api.post();
};
Twinkle.fluff.revertToRevision = function revertToRevision( oldrev ) {
Morebits.status.init( document.getElementById('bodyContent') );
var query = {
'action': 'query',
'prop': ['info', 'revisions'],
'titles': mw.config.get('wgPageName'),
'rvlimit': 1,
'rvstartid': oldrev,
'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ],
'curtimestamp': '',
'meta': 'tokens',
'type': 'csrf',
'format': 'xml'
};
var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of the earlier revision', query, Twinkle.fluff.callbacks.toRevision.main );
wikipedia_api.params = { rev: oldrev };
wikipedia_api.post();
};
Twinkle.fluff.userIpLink = function( user ) {
return (Morebits.isIPAddress(user) ? "[[Special:Contributions/" : "[[User:" ) + user + "|" + user + "]]";
};
Twinkle.fluff.callbacks = {
toRevision: {
main: function( self ) {
var xmlDoc = self.responseXML;
var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10);
var touched = $(xmlDoc).find('page').attr('touched');
var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp');
var edittoken = $(xmlDoc).find('tokens').attr('csrftoken');
var revertToRevID = $(xmlDoc).find('rev').attr('revid');
var revertToUser = $(xmlDoc).find('rev').attr('user');
if (revertToRevID !== self.params.rev) {
self.statitem.error( 'The retrieved revision does not match the requested revision. Aborting.' );
return;
}
var optional_summary = prompt( "Please specify a reason for the revert: ", "" ); // padded out to widen prompt in Firefox
if (optional_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
var summary = "Reverted to revision " + revertToRevID + " by " + revertToUser + (optional_summary ? ": " + optional_summary : '') + "." +
Twinkle.getPref('summaryAd');
var query = {
'action': 'edit',
'title': mw.config.get('wgPageName'),
'summary': summary,
'token': edittoken,
'undo': lastrevid,
'undoafter': revertToRevID,
'basetimestamp': touched,
'starttimestamp': starttimestamp,
'watchlist': Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined,
'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined
};
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Reversion completed";
var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, null/*Twinkle.fluff.callbacks.toRevision.complete*/, self.statelem);
wikipedia_api.params = self.params;
wikipedia_api.post();
},
complete: function (self) {
}
},
main: function( self ) {
var xmlDoc = self.responseXML;
var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10);
var touched = $(xmlDoc).find('page').attr('touched');
var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp');
var edittoken = $(xmlDoc).find('tokens').attr('csrftoken');
var lastuser = $(xmlDoc).find('rev').attr('user');
var revs = $(xmlDoc).find('rev');
if( revs.length < 1 ) {
self.statelem.error( 'We have less than one additional revision, thus impossible to revert' );
return;
}
var top = revs[0];
if( lastrevid < self.params.revid ) {
Morebits.status.error( 'Error', [ 'The most recent revision ID received from the server, ', Morebits.htmlNode( 'strong', lastrevid ), ', is less than the ID of the displayed revision. This could indicate that the current revision has been deleted, the server is lagging, or that bad data has been received. Will stop proceeding at this point.' ] );
return;
}
var index = 1;
if( self.params.revid !== lastrevid ) {
Morebits.status.warn( 'Warning', [ 'Latest revision ', Morebits.htmlNode( 'strong', lastrevid ), ' doesn\'t equal our revision ', Morebits.htmlNode( 'strong', self.params.revid ) ] );
if( lastuser === self.params.user ) {
switch( self.params.type ) {
case 'vand':
Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume vandalism, we continue to revert' ]);
break;
case 'agf':
Morebits.status.warn( 'Warning', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume good faith, we stop reverting, as the problem might have been fixed.' ]);
return;
default:
Morebits.status.warn( 'Notice', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , ', but we will stop reverting anyway.' ] );
return;
}
}
else if(self.params.type === 'vand' &&
Twinkle.fluff.whiteList.indexOf( top.getAttribute( 'user' ) ) !== -1 && revs.length > 1 &&
revs[1].getAttribute( 'pageId' ) === self.params.revid) {
Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', a trusted bot, and the revision before was made by our vandal, so we proceed with the revert.' ] );
index = 2;
} else {
Morebits.status.error( 'Error', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', so it might have already been reverted, stopping reverting.'] );
return;
}
}
if( Twinkle.fluff.whiteList.indexOf( self.params.user ) !== -1 ) {
switch( self.params.type ) {
case 'vand':
Morebits.status.info( 'Info', [ 'Vandalism revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. As this is a whitelisted bot, we assume you wanted to revert vandalism made by the previous user instead.' ] );
index = 2;
vandal = revs[1].getAttribute( 'user' );
self.params.user = revs[1].getAttribute( 'user' );
break;
case 'agf':
Morebits.status.warn( 'Notice', [ 'Good faith revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, it makes no sense at all to revert it as a good faith edit, will stop reverting.' ] );
return;
case 'norm':
/* falls through */
default:
var cont = confirm( 'Normal revert was chosen, but the most recent edit was made by a whitelisted bot (' + self.params.user + '). Do you want to revert the revision before instead?' );
if( cont ) {
Morebits.status.info( 'Info', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, and per confirmation, we\'ll revert the previous revision instead.' ] );
index = 2;
self.params.user = revs[1].getAttribute( 'user' );
} else {
Morebits.status.warn( 'Notice', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, but per confirmation, revert on top revision will proceed.' ] );
}
break;
}
}
var found = false;
var count = 0;
for( var i = index; i < revs.length; ++i ) {
++count;
if( revs[i].getAttribute( 'user' ) !== self.params.user ) {
found = i;
break;
}
}
if( ! found ) {
self.statelem.error( [ 'No previous revision found. Perhaps ', Morebits.htmlNode( 'strong', self.params.user ), ' is the only contributor, or that the user has made more than ' + Twinkle.getPref('revertMaxRevisions') + ' edits in a row.' ] );
return;
}
if( ! count ) {
Morebits.status.error( 'Error', "We were to revert zero revisions. As that makes no sense, we'll stop reverting this time. It could be that the edit has already been reverted, but the revision ID was still the same." );
return;
}
var good_revision = revs[ found ];
var userHasAlreadyConfirmedAction = false;
if (self.params.type !== 'vand' && count > 1) {
if ( !confirm( self.params.user + ' has made ' + count + ' edits in a row. Are you sure you want to revert them all?') ) {
Morebits.status.info( 'Notice', 'Stopping reverting per user input' );
return;
}
userHasAlreadyConfirmedAction = true;
}
self.params.count = count;
self.params.goodid = good_revision.getAttribute( 'revid' );
self.params.gooduser = good_revision.getAttribute( 'user' );
self.statelem.status( [ ' revision ', Morebits.htmlNode( 'strong', self.params.goodid ), ' that was made ', Morebits.htmlNode( 'strong', count ), ' revisions ago by ', Morebits.htmlNode( 'strong', self.params.gooduser ) ] );
var summary, extra_summary, userstr, gooduserstr;
switch( self.params.type ) {
case 'agf':
extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox
if (extra_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
userHasAlreadyConfirmedAction = true;
userstr = self.params.user;
summary = "Reverted good faith edits by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" +
userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd');
break;
case 'vand':
userstr = self.params.user;
gooduserstr = self.params.gooduser;
summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" +
userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]]) identified as vandalism to last revision by " +
gooduserstr + "." + Twinkle.getPref('summaryAd');
break;
case 'norm':
/* falls through */
default:
if( Twinkle.getPref('offerReasonOnNormalRevert') ) {
extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox
if (extra_summary === null)
{
self.statelem.error( 'Aborted by user.' );
return;
}
userHasAlreadyConfirmedAction = true;
}
userstr = self.params.user;
summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" +
userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) +
Twinkle.getPref('summaryAd');
break;
}
if (Twinkle.getPref('confirmOnFluff') && !userHasAlreadyConfirmedAction && !confirm("Reverting page: are you sure?")) {
self.statelem.error( 'Aborted by user.' );
return;
}
var query;
if( (!self.params.autoRevert || Twinkle.getPref('openTalkPageOnAutoRevert')) &&
Twinkle.getPref('openTalkPage').indexOf( self.params.type ) !== -1 &&
mw.config.get('wgUserName') !== self.params.user ) {
Morebits.status.info( 'Info', [ 'Opening user talk page edit form for user ', Morebits.htmlNode( 'strong', self.params.user ) ] );
query = {
'title': 'User talk:' + self.params.user,
'action': 'edit',
'preview': 'yes',
'vanarticle': self.params.pagename.replace(/_/g, ' '),
'vanarticlerevid': self.params.revid,
'vanarticlegoodrevid': self.params.goodid,
'type': self.params.type,
'count': self.params.count
};
switch( Twinkle.getPref('userTalkPageMode') ) {
case 'tab':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' );
break;
case 'blank':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
case 'window':
/* falls through */
default:
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
}
}
query = {
'action': 'edit',
'title': self.params.pagename,
'summary': summary,
'token': edittoken,
'undo': lastrevid,
'undoafter': self.params.goodid,
'basetimestamp': touched,
'starttimestamp': starttimestamp,
'watchlist' : Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined,
'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined
};
Morebits.wiki.actionCompleted.redirect = self.params.pagename;
Morebits.wiki.actionCompleted.notice = "Reversion completed";
var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, Twinkle.fluff.callbacks.complete, self.statelem);
wikipedia_api.params = self.params;
wikipedia_api.post();
},
complete: function (self) {
self.statelem.info("done");
}
};
Twinkle.fluff.formatSummaryPostfix = function(stringToAdd) {
if (stringToAdd) {
stringToAdd = ': ' + Morebits.string.toUpperCaseFirstChar(stringToAdd);
if (stringToAdd.search(/[.?!;]$/) === -1) {
stringToAdd = stringToAdd + '.';
}
return stringToAdd;
}
else {
return '.';
}
};
Twinkle.fluff.init = function twinklefluffinit() {
if (twinkleUserAuthorized)
{
// a list of usernames, usually only bots, that vandalism revert is jumped over, that is
// if vandalism revert was chosen on such username, then it's target is on the revision before.
// This is for handeling quick bots that makes edits seconds after the original edit is made.
// This only affect vandalism rollback, for good faith rollback, it will stop, indicating a bot
// has no faith, and for normal rollback, it will rollback that edit.
Twinkle.fluff.whiteList = [
'AnomieBOT',
'ClueBot NG',
'SineBot'
];
if ( Morebits.queryString.exists( 'twinklerevert' ) ) {
Twinkle.fluff.auto();
} else {
Twinkle.fluff.normal();
}
}
};
/*
****************************************
*** twinklespeedy.js: CSD module
****************************************
* Mode of invocation: Tab ("CSD")
* Active on: Non-special, existing pages
* Config directives in: TwinkleConfig
*
* NOTE FOR DEVELOPERS:
* If adding a new criterion, check out the default values of the CSD preferences
* in twinkle.header.js, and add your new criterion to those if you think it would
* be good.
*/
Twinkle.speedy = function twinklespeedy() {
// Disable on:
// * special pages
// * non-existent pages
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
twAddPortletLink( Twinkle.speedy.callback, "Del", "tw-csd", Morebits.userIsInGroup('sysop') ? "Delete page" : "Request deletion" );
};
// This function is run when the CSD tab/header link is clicked
Twinkle.speedy.callback = function twinklespeedyCallback() {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
Twinkle.speedy.initDialog(Morebits.userIsInGroup( 'sysop' ) ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true);
};
Twinkle.speedy.dialog = null; // used by unlink feature
// Prepares the speedy deletion dialog and displays it
Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) {
var dialog;
Twinkle.speedy.dialog = new Morebits.simpleWindow( Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight') );
dialog = Twinkle.speedy.dialog;
dialog.setTitle( "Choose criteria for deletion" );
dialog.setScriptName( "Twinkle" );
//dialog.addFooterLink( "Quick deletion policy", "Wikipedia:Deletion policy#Quick deletion" );
dialog.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#speedy" );
var form = new Morebits.quickForm( callbackfunc, (Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null) );
if( Morebits.userIsInGroup( 'sysop' ) ) {
form.append( {
type: 'checkbox',
list: [
{
label: 'Tag page only, don\'t delete',
value: 'tag_only',
name: 'tag_only',
tooltip: 'If you just want to tag the page, instead of deleting it now',
checked : Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
var cForm = event.target.form;
var cChecked = event.target.checked;
// enable/disable talk page checkbox
if (cForm.talkpage) {
cForm.talkpage.disabled = cChecked;
cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete');
}
// enable/disable redirects checkbox
cForm.redirects.disabled = cChecked;
cForm.redirects.checked = !cChecked;
// enable/disable notify checkbox
cForm.notify.disabled = !cChecked;
cForm.notify.checked = cChecked;
// enable/disable multiple
cForm.multiple.disabled = !cChecked;
cForm.multiple.checked = false;
Twinkle.speedy.callback.dbMultipleChanged(cForm, false);
event.stopPropagation();
}
}
]
} );
form.append( { type: 'header', label: 'Delete-related options' } );
if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page
form.append( {
type: 'checkbox',
list: [
{
label: 'Also delete talk page',
value: 'talkpage',
name: 'talkpage',
tooltip: "This option deletes the page's talk page in addition. If you choose the F8 (moved to Commons) criterion, this option is ignored and the talk page is *not* deleted.",
checked: Twinkle.getPref('deleteTalkPageOnDelete'),
disabled: Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
}
form.append( {
type: 'checkbox',
list: [
{
label: 'Also delete all redirects',
value: 'redirects',
name: 'redirects',
tooltip: "This option deletes all incoming redirects in addition. Avoid this option for procedural (e.g. move/merge) deletions.",
checked: true,
disabled: Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
form.append( { type: 'header', label: 'Tag-related options' } );
}
form.append( {
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notify',
tooltip: "A notification template will be placed on the talk page of the creator, IF you have a notification enabled in your Twinkle preferences " +
"for the criterion you choose AND this box is checked. The creator may be welcomed as well.",
checked: !Morebits.userIsInGroup( 'sysop' ) || Twinkle.getPref('deleteSysopDefaultToTag'),
disabled: Morebits.userIsInGroup( 'sysop' ) && !Twinkle.getPref('deleteSysopDefaultToTag'),
event: function( event ) {
event.stopPropagation();
}
}
]
} );
form.append( {
type: 'div',
name: 'work_area',
label: 'Failed to initialize the CSD module. Please try again, or tell the Twinkle developers about the issue.'
} );
if( Twinkle.getPref( 'speedySelectionStyle' ) !== 'radioClick' ) {
form.append( { type: 'submit' } );
}
var result = form.render();
dialog.setContent( result );
dialog.display();
Twinkle.speedy.callback.dbMultipleChanged( result, false );
};
Twinkle.speedy.callback.dbMultipleChanged = function twinklespeedyCallbackDbMultipleChanged(form, checked) {
var namespace = mw.config.get('wgNamespaceNumber');
var value = checked;
var work_area = new Morebits.quickForm.element( {
type: 'div',
name: 'work_area'
} );
if (checked && Twinkle.getPref('speedySelectionStyle') === 'radioClick') {
work_area.append( {
type: 'div',
label: 'When finished choosing criteria, click:'
} );
work_area.append( {
type: 'button',
name: 'submit-multiple',
label: 'Submit Query',
event: function( event ) {
Twinkle.speedy.callback.evaluateUser( event );
event.stopPropagation();
}
} );
}
var radioOrCheckbox = (value ? 'checkbox' : 'radio');
/*
if (namespace % 2 === 1 && namespace !== 3) { // talk pages, but not user talk pages
work_area.append( { type: 'header', label: 'Talk pages' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.talkList } );
}
switch (namespace) {
case 0: // article
case 1: // talk
work_area.append( { type: 'header', label: 'Articles' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getArticleList(value) } );
break;
case 2: // user
case 3: // user talk
work_area.append( { type: 'header', label: 'User pages' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.userList } );
break;
case 6: // file
case 7: // file talk
work_area.append( { type: 'header', label: 'Files' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getFileList(value) } );
break;
case 10: // template
case 11: // template talk
work_area.append( { type: 'header', label: 'Templates' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.templateList } );
break;
case 14: // category
case 15: // category talk
work_area.append( { type: 'header', label: 'Categories' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.categoryList } );
break;
default:
break;
}
*/
work_area.append( { type: 'header', label: 'General criteria' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getGeneralList(value) });
/*
work_area.append( { type: 'header', label: 'Redirects' } );
work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.redirectList } );
*/
var old_area = Morebits.quickForm.getElements(form, "work_area")[0];
form.replaceChild(work_area.render(), old_area);
};
Twinkle.speedy.talkList = [
/*{
label: 'G8: Talk pages with no page belonging to it',
value: 'talk',
tooltip: 'This does not include any page that is useful to the project - for example user talk pages, talk page archives, and talk pages for files that exist on Wikimedia Commons.'
}*/
];
// this is a function to allow for db-multiple filtering
Twinkle.speedy.getFileList = function twinklespeedyGetFileList(multiple) {
var result = [];
/*result.push({
label: 'F1: Not allowed',
value: 'prohibitedimage',
tooltip: 'Most media uploads are not allowed on Simple English Wikipedia. They should be uploaded to Wikimedia Commons instead. There are a few exceptions to this rule. Firstly, all spoken articles should be uploaded here, as they are for local use. Secondly, there are some logos that Commons does not accept, but are needed here, for example Image:Wiki.png, which is used as the Wikipedia logo.'
});*/
return result;
};
Twinkle.speedy.getArticleList = function twinklespeedyGetArticleList(multiple) {
var result = [];
/*result.push({
label: 'A1: Little or no meaning',
value: 'nocontext',
tooltip: 'Is very short and providing little or no meaning (e.g., "He is a funny man that has created Factory and the Hacienda. And, by the way, his wife is great."). Having a small amount of content is not a reason to delete if it has useful information.'
});
result.push({
label: 'A2: No content',
value: 'nocontent',
tooltip: 'Has no content. This includes any article consisting only of links (including hyperlinks, category tags and "see also" sections), a rephrasing of the title, and/or attempts to correspond with the person or group named by its title. This does not include disambiguation pages.'
});
result.push({
label: 'A3: Article that exists on another Wikimedia project',
value: 'transwiki',
tooltip: 'Has been copied and pasted from another Wikipedia: Any article or section from an article that has been copied and pasted with little or no change.'
});
result.push({
label: 'A4: People, groups, companies, products, services or websites that do not claim to be notable.',
value: 'notability',
tooltip: 'An article about a real person, group of people, band, club, company, product, service or or web content that does not say why it is important. If not everyone agrees that the subject is not notable or there has been a previous RfD, the article may not be quickly deleted, and should be discussed at RfD instead.'
});
result.push({
label: 'A5: Not written in English',
value: 'foreign',
tooltip: 'Any article that is not written in English. An article that is written in any other languages but English.'
});
result.push({
label: 'A6: Obvious hoax',
value: 'hoax',
tooltip: 'Is an obvious hoax. An article that is surely fake or impossible.'
});*/
return result;
};
Twinkle.speedy.categoryList = [
/*{
label: 'C1: Empty categories',
value: 'catempty',
tooltip: '(with no articles or subcategories for at least four days) whose only content includes links to parent categories. However, this can not be used on categories still being discussed on WP:RfD, or disambiguation categories. If the category wasn\'t newly made, it is possible that it used to have articles, and more inspection is needed.'
},
{
label: 'C2: Quick renaming',
value: 'catqr',
tooltip: 'Empty categories that have already been renamed.'
},
{
label: 'C3: Template categories',
value: 'catfd',
tooltip: 'If a category contains articles from only one template (such as Category:Cleanup needed from \{\{cleanup\}\}) and the template is deleted after being discussed, the category can also be deleted without being discussed.'
}*/
];
Twinkle.speedy.userList = [
/*{
label: 'U1: User request',
value: 'userreq',
tooltip: 'User pages can be deleted if its user wants to, but there are some exceptions.'
},
{
label: 'U2: Nonexistent user',
value: 'nouser',
tooltip: 'User pages of users that do not exist. Administrators should check Special:Contributions and Special:DeletedContributions.'
}*/
];
Twinkle.speedy.templateList = [
/*{
label: 'T2: They are deprecated or replaced by a newer template and are completely unused and not linked to.',
value: 'replaced',
tooltip: 'For any template that should not be deleted quickly, use Wikipedia:Requests for deletion.'
}*/
//});
// return result;
];
Twinkle.speedy.getGeneralList = function twinklespeedyGetGeneralList(multiple) {
var result = [];
if (!multiple) {
result.push({
label: 'Custom rationale' + (Morebits.userIsInGroup('sysop') ? ' (custom deletion reason)' : ' using {'+'{delete|reason}}'),
value: 'reason',
tooltip: 'You can enter an custom reason.'
});
}
/*result.push({
label: 'G1: Nonsense',
value: 'nonsense',
tooltip: 'All of the text is nonsense. Nonsense includes content that does not make sense or is not meaningful. However, this does not include bad writing, bad words, vandalism, things that are fake or impossible, or parts which are not in English. '
});
result.push({
label: 'G2: Test page',
value: 'test',
tooltip: 'It is a test page, such as "Can I really create a page here?".'
});
result.push({
label: 'G3: Complete vandalism',
value: 'vandalism',
tooltip: 'The content is completely vandalism.'
});
result.push({
label: 'G4: Recreation of deleted material already deleted at RfD',
value: 'repost',
tooltip: 'Creation of content that is already deleted. It includes an identical or similar copy, with any title, of a page that was deleted, after being discussed in Requests for deletion, unless it was undeleted due to another discussion or was recreated in the user space. Before deleting again, the Administrator should be sure that the content is similar and not just a new article on the same subject. This rule cannot be used if the content had already been quickly deleted before.'
});
if (!multiple) {
result.push({
label: 'G6: History merge',
value: 'histmerge',
tooltip: 'Temporarily deleting a page in order to merge page histories'
});
result.push({
label: 'G6: Move',
value: 'move',
tooltip: 'Making way for a noncontroversial move like reversing a redirect'
});
result.push({
label: 'G6: RfD',
value: 'afd',
tooltip: 'An admin has closed a RfD as "delete".'
});
}
result.push({
label: 'G6: Housekeeping',
value: 'g6',
tooltip: 'Other non-controversial "housekeeping" tasks'
});
result.push({
label: 'G7: Author requests deletion, or author blanked',
value: 'author',
tooltip: 'Any page whose original author wants deletion, can be quickly deleted, but only if most of the page was written by that author and was created as a mistake. If the author blanks the page, this can mean that he or she wants it deleted.'
});
result.push({
label: 'G8: Pages dependent on a non-existent or deleted page',
value: 'talk',
tooltip: '... can be deleted, unless they contain discussion on deletion that can\'t be found anywhere else. Subpages of a talk page can only be deleted under this rule if their top-level page does not exist. This also applies to broken redirects. However, this cannot be used on user talk pages or talk pages of images on Commons.'
});
if (!multiple) {
result.push({
label: 'G8: Subpages with no parent page',
value: 'subpage',
tooltip: 'This excludes any page that is useful to the project, and in particular: deletion discussions that are not logged elsewhere, user and user talk pages, talk page archives, plausible redirects that can be changed to valid targets, and file pages or talk pages for files that exist on Wikimedia Commons.'
});
}
result.push({
label: 'G10: Attack page',
value: 'attack',
tooltip: 'Pages that were only created to insult a person or thing (such as "John Q. Doe is dumb"). This includes articles on a living person that is insult and without sources, where there is no NPOV version in the edit history to revert to.'
});
result.push({
label: 'G11: Obvious advertising',
value: 'spam',
tooltip: 'Pages which were created only to say good things about a company, item, group or service and which would need to be written again so that they can sound like an encyclopedia. However, simply having a company, item, group or service as its subject does not mean that an article can be deleted because of this rule: an article that is obvious advertising should have content that shouldn\'t be in an encyclopedia. If a page has already gone through RfD or QD and was not deleted, it should not be quickly deleted using this rule.'
});
result.push({
label: 'G12: Obviously breaking copyright law',
value: 'copyvio',
tooltip: 'Obviously breaking copyright law like a page which is 1) Copied from another website which does not have a license that can be used with Wikipedia; 2) Containing no content in the page history that is worth being saved. 3) Made by one person instead of being created on wiki and then copied by another website such as one of the many Wikipedia mirror websites. 4) Added by someone who doesn\'t tell if he got permission to do so or not, or if his claim has a large chance of not being true;'
});*/
return result;
};
Twinkle.speedy.redirectList = [
/*{
label: 'R1: Redirects to a non-existent page.',
value: 'redirnone',
tooltip: 'Redirects to a non-existent page.'
},
{
label: 'R2: Redirects from mainspace to any other namespace except the Category:, Template:, Wikipedia:, Help: and Portal: namespaces',
value: 'rediruser',
tooltip: '(this does not include the Wikipedia shortcut pseudo-namespaces). If this was the result of a page move, consider waiting a day or two before deleting the redirect'
},
{
label: 'R3: Redirects as a result of an implausible typo that were recently created',
value: 'redirtypo',
tooltip: 'However, redirects from common misspellings or misnomers are generally useful, as are redirects in other languages'
}*/
];
Twinkle.speedy.normalizeHash = {
'reason': 'db',
'nonsense': 'g1',
'test': 'g2',
'vandalism': 'g3',
'hoax': 'g3',
'repost': 'g4',
'histmerge': 'g6',
'move': 'g6',
'afd': 'g6',
'g6': 'g6',
'author': 'g7',
'talk': 'g8',
'subpage': 'g8',
'attack': 'g10',
'spam': 'g11',
'copyvio': 'g12',
'nocontext': 'a1',
'nocontent': 'a2',
'transwiki': 'a3',
'notability': 'a4',
'foreign': 'a5',
'hoax': 'a6',
'redirnone': 'r1',
'rediruser': 'r2',
'redirtypo': 'r3',
'prohibitedimage': 'f1',
'catempty': 'c1',
'catqr': 'c2',
'catfd': 'c3',
'userreq': 'u1',
'nouser': 'u2',
'replaced':'t2'
};
// keep this synched with [[MediaWiki:Deletereason-dropdown]]
Twinkle.speedy.reasonHash = {
'reason': '',
'nonsense': 'was all nonsense',
'test': 'was a test page',
'vandalism': 'was vandalism',
'pagemove': 'was a redirect created during cleanup of page move vandalism',
'repost': 'was a copy of a page that was deleted by RfD',
'histmerge': 'was in the way of trying to fix or clean up something',
'move': 'was in the way of making a move',
'afd': 'was closed as delete in a RfD',
'g6': 'was housekeeping',
'author': 'was asked to be deleted by the author',
'blanked': 'was implied to be deleted by the author',
'talk': 'was a talk page of a page that does not exist',
'attack': 'was an attack page',
'spam': 'was advertising',
'copyvio': 'was breaking copyright law',
'nocontext': 'was a page that had little or no meaning',
'nocontent': 'was a page that had no content',
'transwiki': 'was copied from another Wikipedia',
'notability': 'was a page that didn\'t say why the subject was notable',
'foreign': 'was not written in English',
'hoax': 'was obviously a hoax (not true)',
'redirnone': 'was a redirect to a page that does not exist',
'rediruser': 'was a redirect to the Talk:, User: or User talk: space',
'redirtypo': 'was a redirect with an uncommon typo',
'prohibitedimage': 'was an image/media that is not allowed on Wikipedia',
'catempty': 'was an empty category',
'catqr': 'was a renamed category',
'catfd': 'was a category containing articles from a now deleted template',
'userreq': 'was a user page whose user requested deletion',
'nouser': 'was a user page of a user that did not exist',
'replaced': 'was deprecated or replaced by a newer template and are completely unused and not linked to'
};
Twinkle.speedy.callbacks = {
sysop: {
main: function( params ) {
var thispage = new Morebits.wiki.page( mw.config.get('wgPageName'), "Deleting page" );
// delete page
var reason;
if (params.normalized === 'db') {
reason = prompt("Enter the deletion summary to use, which will be entered into the deletion log:", "");
} else {
var presetReason = "[[WP:QD#" + params.normalized.toUpperCase() + "|" + params.normalized.toUpperCase() + "]]: " + params.reason;
if (Twinkle.getPref("promptForSpeedyDeletionSummary").indexOf(params.normalized) !== -1) {
reason = prompt("Enter the deletion summary to use, or press OK to accept the automatically generated one.", presetReason);
} else {
reason = presetReason;
}
}
if (!reason || !reason.replace(/^\s*/, "").replace(/\s*$/, "")) {
Morebits.status.error("Asking for reason", "you didn't give one. I don't know... what with admins and their apathetic antics... I give up...");
return;
}
thispage.setEditSummary( reason + Twinkle.getPref('deletionSummaryAd') );
thispage.deletePage();
// delete talk page
if (params.deleteTalkPage &&
params.normalized !== 'f8' &&
document.getElementById( 'ca-talk' ).className !== 'new') {
var talkpage = new Morebits.wiki.page( Morebits.wikipedia.namespaces[ mw.config.get('wgNamespaceNumber') + 1 ] + ':' + mw.config.get('wgTitle'), "Deleting talk page" );
talkpage.setEditSummary('Talk page of deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd'));
talkpage.deletePage();
}
// promote Unlink tool
var $link, $bigtext;
if( mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f8' ) {
$link = $('<a/>', {
'href': '#',
'text': 'click here to go to the Unlink tool',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' },
'click': function(){
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback("Removing usages of and/or links to deleted file " + mw.config.get('wgPageName'));
}
});
$bigtext = $('<span/>', {
'text': 'To orphan backlinks and remove instances of file usage',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' }
});
Morebits.status.info($bigtext[0], $link[0]);
} else if (params.normalized !== 'f8') {
$link = $('<a/>', {
'href': '#',
'text': 'click here to go to the Unlink tool',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' },
'click': function(){
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback("Removing links to deleted page " + mw.config.get('wgPageName'));
}
});
$bigtext = $('<span/>', {
'text': 'To orphan backlinks',
'css': { 'fontSize': '130%', 'fontWeight': 'bold' }
});
Morebits.status.info($bigtext[0], $link[0]);
}
// open talk page of first contributor
if( params.openusertalk ) {
thispage = new Morebits.wiki.page( mw.config.get('wgPageName') ); // a necessary evil, in order to clear incorrect status text
thispage.setCallbackParameters( params );
thispage.lookupCreator( Twinkle.speedy.callbacks.sysop.openUserTalkPage );
}
// delete redirects
if (params.deleteRedirects) {
var query = {
'action': 'query',
'list': 'backlinks',
'blfilterredir': 'redirects',
'bltitle': mw.config.get('wgPageName'),
'bllimit': 5000 // 500 is max for normal users, 5000 for bots and sysops
};
var wikipedia_api = new Morebits.wiki.api( 'getting list of redirects...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain,
new Morebits.status( 'Deleting redirects' ) );
wikipedia_api.params = params;
wikipedia_api.post();
}
},
openUserTalkPage: function( pageobj ) {
pageobj.getStatusElement().unlink(); // don't need it anymore
var user = pageobj.getCreator();
var statusIndicator = new Morebits.status('Opening user talk page edit form for ' + user, 'opening...');
var query = {
'title': 'User talk:' + user,
'action': 'edit',
'preview': 'yes',
'vanarticle': mw.config.get('wgPageName').replace(/_/g, ' ')
};
switch( Twinkle.getPref('userTalkPageMode') ) {
case 'tab':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' );
break;
case 'blank':
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
case 'window':
/* falls through */
default :
window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' );
break;
}
statusIndicator.info( 'complete' );
},
deleteRedirectsMain: function( apiobj ) {
var xmlDoc = apiobj.getXML();
var $snapshot = $(xmlDoc).find('backlinks bl');
var total = $snapshot.length;
if( !total ) {
return;
}
var statusIndicator = apiobj.statelem;
statusIndicator.status("0%");
var onsuccess = function( apiobj ) {
var obj = apiobj.params.obj;
var total = apiobj.params.total;
var now = parseInt( 100 * ++(apiobj.params.current)/total, 10 ) + '%';
obj.update( now );
apiobj.statelem.unlink();
if( apiobj.params.current >= total ) {
obj.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
var params = $.extend( {}, apiobj.params );
params.current = 0;
params.total = total;
params.obj = statusIndicator;
$snapshot.each(function(key, value) {
var title = $(value).attr('title');
var page = new Morebits.wiki.page(title, 'Deleting redirect "' + title + '"');
page.setEditSummary('Redirect to deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd'));
page.deletePage(onsuccess);
});
}
},
user: {
main: function(pageobj) {
var statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error( "It seems that the page doesn't exist; perhaps it has already been deleted" );
return;
}
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
statelem.status( 'Checking for tags on the page...' );
// check for existing deletion tags
var tag = /(?:\{\{\s*(qd|qd-multiple|db|delete|db-.*?)(?:\s*\||\s*\}\}))/.exec( text );
if( tag ) {
statelem.error( [ Morebits.htmlNode( 'strong', tag[1] ) , " is already placed on the page." ] );
return;
}
var xfd = /(?:\{\{([rsaiftcm]fd|md1|proposed deletion)[^{}]*?\}\})/i.exec( text );
if( xfd && !confirm( "The deletion-related template {{" + xfd[1] + "}} was found on the page. Do you still want to add a CSD template?" ) ) {
return;
}
var code, parameters, i;
if (params.normalizeds.length > 1)
{
code = "{{QD-multiple";
var breakFlag = false;
$.each(params.normalizeds, function(index, norm) {
code += "|" + norm.toUpperCase();
parameters = Twinkle.speedy.getParameters(params.values[index], norm, statelem);
if (!parameters) {
breakFlag = true;
return false; // the user aborted
}
for (i in parameters) {
if (typeof parameters[i] === 'string' && !parseInt(i, 10)) { // skip numeric parameters - {{db-multiple}} doesn't understand them
code += "|" + i + "=" + parameters[i];
}
}
});
if (breakFlag) {
return;
}
code += "}}";
params.utparams = [];
}
else
{
parameters = Twinkle.speedy.getParameters(params.values[0], params.normalizeds[0], statelem);
if (!parameters) {
return; // the user aborted
}
code = "{{delete|" + params.normalizeds;
for (i in parameters) {
if (typeof parameters[i] === 'string') {
code += "|" + i + "=" + parameters[i];
}
}
code += "|editor=" + mw.config.get("wgUserName") + "|date=~~~~~";
code += "}}";
params.utparams = Twinkle.speedy.getUserTalkParameters(params.normalizeds[0], parameters);
}
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
// patrol the page, if reached from Special:NewPages
if( Twinkle.getPref('markSpeedyPagesAsPatrolled') ) {
thispage.patrol();
}
// Wrap SD template in noinclude tags if we are in template space.
// Won't work with userboxes in userspace, or any other transcluded page outside template space
if (mw.config.get('wgNamespaceNumber') === 10) { // Template:
code = "<noinclude>" + code + "</noinclude>";
}
// Remove tags that become superfluous with this action
if (mw.config.get('wgNamespaceNumber') === 6) {
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = text.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)[^}]*\}\}/gi, "");
}
// Generate edit summary for edit
var editsummary;
if (params.normalizeds.length > 1) {
editsummary = 'Requesting quick deletion (';
$.each(params.normalizeds, function(index, norm) {
editsummary += '[[WP:QD#' + norm.toUpperCase() + '|QD ' + norm.toUpperCase() + ']], ';
});
editsummary = editsummary.substr(0, editsummary.length - 2); // remove trailing comma
editsummary += ').';
} else if (params.normalizeds[0] === "db") {
editsummary = 'Requesting deletion with criteria \"' + parameters["1"] + '\".';
} else if (params.values[0] === "histmerge") {
editsummary = "Requesting history merge with [[" + parameters["1"] + "]]";
} else {
editsummary = "Requesting deletion (" + params.normalizeds[0].toUpperCase() + ")";
}
pageobj.setPageText(code + ((params.normalizeds.indexOf('g10') !== -1) ? '' : ("\n" + text) )); // cause attack pages to be blanked
pageobj.setEditSummary(editsummary + Twinkle.getPref('summaryAd'));
pageobj.setWatchlist(params.watch);
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.speedy.callbacks.user.tagComplete);
},
tagComplete: function(pageobj) {
var params = pageobj.getCallbackParameters();
// Notification to first contributor
if (params.usertalk) {
var callback = function(pageobj) {
var initialContrib = pageobj.getCreator();
// don't notify users when their user talk page is nominated
if (initialContrib === mw.config.get('wgTitle') && mw.config.get('wgNamespaceNumber') === 3) {
Morebits.status.warn("Notifying initial contributor: this user created their own user talk page; skipping notification");
return;
}
var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"),
notifytext, i;
// specialcase "db" and "db-multiple"
if (params.normalizeds.length > 1) {
notifytext = "\n{{subst:QD-notice-multiple|page=" + mw.config.get('wgPageName');
var count = 2;
$.each(params.normalizeds, function(index, norm) {
notifytext += "|" + (count++) + "=" + norm.toUpperCase();
});
} else if (params.normalizeds[0] === "db") {
notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds;
} else {
notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds;
}
for (i in params.utparams) {
if (typeof params.utparams[i] === 'string') {
notifytext += "|" + i + "=" + params.utparams[i];
}
}
notifytext += (params.welcomeuser ? "" : "|nowelcome=yes") + "}} ~~~~";
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary("Notification: quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
usertalkpage.setCreateOption('recreate');
usertalkpage.setFollowRedirect(true);
usertalkpage.append();
// add this nomination to the user's userspace log, if the user has enabled it
if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params, initialContrib);
}
};
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
thispage.lookupCreator(callback);
}
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
else if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params, null);
}
},
// note: this code is also invoked from twinkleimage
// the params used are:
// for CSD: params.values, params.normalizeds (note: normalizeds is an array)
// for DI: params.fromDI = true, params.type, params.normalized (note: normalized is a string)
addToLog: function(params, initialContrib) {
var wikipedia_page = new Morebits.wiki.page("User:" + mw.config.get('wgUserName') + "/" + Twinkle.getPref('speedyLogPageName'), "Adding entry to userspace log");
params.logInitialContrib = initialContrib;
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.saveLog);
},
saveLog: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
// add blurb if log page doesn't exist
if (!pageobj.exists()) {
text =
"This is a log of all deletion nominations made by this user using [[mh:dev:Twinkle|Twinkle]]'s QD module.\n\n" +
"If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and " +
"request deletion for this page.\n";
if (Morebits.userIsInGroup("sysop")) {
text += "\nThis log does not track outright speedy deletions made using Twinkle.\n";
}
}
// create monthly header
var date = new Date();
var headerRe = new RegExp("^==+\\s*" + date.getUTCMonthName() + "\\s+" + date.getUTCFullYear() + "\\s*==+", "m");
if (!headerRe.exec(text)) {
text += "\n\n=== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ===";
}
text += "\n# [[:" + mw.config.get('wgPageName') + "]]: ";
if (params.fromDI) {
text += "DI [[WP:QD#" + params.normalized.toUpperCase() + "|QD " + params.normalized.toUpperCase() + "]] (" + params.type + ")";
} else {
if (params.normalizeds.length > 1) {
text += "multiple criteria (";
$.each(params.normalizeds, function(index, norm) {
text += "[[WP:QD#" + norm.toUpperCase() + "|" + norm.toUpperCase() + ']], ';
});
text = text.substr(0, text.length - 2); // remove trailing comma
text += ')';
} else if (params.normalizeds[0] === "db") {
text += "{{tl|QD}}";
} else {
text += "[[WP:QD#" + params.normalizeds[0].toUpperCase() + "|CSD " + params.normalizeds[0].toUpperCase() + "]] ({{tl|db-" + params.values[0] + "}})";
}
}
if (params.logInitialContrib) {
text += "; notified {{user|" + params.logInitialContrib + "}}";
}
text += " ~~~~~\n";
pageobj.setPageText(text);
pageobj.setEditSummary("Logging quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption("recreate");
pageobj.save();
}
}
};
// prompts user for parameters to be passed into the speedy deletion tag
Twinkle.speedy.getParameters = function twinklespeedyGetParameters(value, normalized, statelem)
{
var parameters = [];
switch( normalized ) {
case 'db':
var dbrationale = prompt('Please enter a custom reason. \n\"This page can be quickly deleted because:\"', "");
if (!dbrationale || !dbrationale.replace(/^\s*/, "").replace(/\s*$/, ""))
{
statelem.error( 'You must specify a reason. Aborted by user.' );
return null;
}
parameters["1"] = dbrationale;
break;
case 'g12':
var url = prompt( '[QD G12] Please enter the URL if available, including the "http://":', "" );
if (url === null)
{
statelem.error( 'Aborted by user.' );
return null;
}
parameters.url = url;
break;
default:
var defaultreason = prompt('You can enter more details here. \n' +
"Just click OK if you don't want or need to.", "");
if (defaultreason === null) {
return true; // continue to next tag
} else if (defaultreason !== "") {
parameters["2"] = defaultreason;
}
break;
}
return parameters;
};
// function for processing talk page notification template parameters
Twinkle.speedy.getUserTalkParameters = function twinklespeedyGetUserTalkParameters(normalized, parameters)
{
var utparams = [];
switch (normalized)
{
case 'db':
utparams["2"] = parameters["1"];
break;
case 'a10':
utparams.key1 = "article";
utparams.value1 = parameters.article;
break;
default:
break;
}
return utparams;
};
Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) {
var values = (e.target.form ? e.target.form : e.target).getChecked('csd');
if (values.length === 0) {
alert( "Please select a criterion!" );
return null;
}
return values;
};
Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e)
{
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var form = (e.target.form ? e.target.form : e.target);
var tag_only = form.tag_only;
if( tag_only && tag_only.checked ) {
Twinkle.speedy.callback.evaluateUser(e);
return;
}
var value = Twinkle.speedy.resolveCsdValues(e)[0];
if (!value) {
return;
}
var normalized = Twinkle.speedy.normalizeHash[ value ];
var params = {
value: value,
normalized: normalized,
watch: Twinkle.getPref('watchSpeedyPages').indexOf( normalized ) !== -1,
reason: Twinkle.speedy.reasonHash[ value ],
openusertalk: Twinkle.getPref('openUserTalkPageOnSpeedyDelete').indexOf( normalized ) !== -1,
deleteTalkPage: form.talkpage && form.talkpage.checked,
deleteRedirects: form.redirects.checked
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Twinkle.speedy.callbacks.sysop.main( params );
};
Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var form = (e.target.form ? e.target.form : e.target);
if (e.target.type === "checkbox") {
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
//var multiple = form.multiple.checked;
var normalizeds = [];
$.each(values, function(index, value) {
var norm = Twinkle.speedy.normalizeHash[ value ];
// for sysops only
if (['f4', 'f5', 'f6', 'f11'].indexOf(norm) !== -1) {
alert("Tagging with F4, F5, F6, and F11 is not possible using the CSD module. Try using DI instead, or unchecking \"Tag page only\" if you meant to delete the page.");
return;
}
normalizeds.push(norm);
});
// analyse each criterion to determine whether to watch the page/notify the creator
var watchPage = false;
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('watchSpeedyPages').indexOf(norm) !== -1) {
watchPage = true;
return false; // break
}
});
var notifyuser = false;
if (form.notify.checked) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('notifyUserOnSpeedyDeletionNomination').indexOf(norm) !== -1) {
notifyuser = true;
return false; // break
}
});
}
var welcomeuser = false;
if (notifyuser) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('welcomeUserOnSpeedyDeletionNotification').indexOf(norm) !== -1) {
welcomeuser = true;
return false; // break
}
});
}
var csdlog = false;
if (Twinkle.getPref('logSpeedyNominations')) {
$.each(normalizeds, function(index, norm) {
if (Twinkle.getPref('noLogOnSpeedyNomination').indexOf(norm) === -1) {
csdlog = true;
return false; // break
}
});
}
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
usertalk: notifyuser,
welcomeuser: welcomeuser,
lognomination: csdlog
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( form );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Tagging complete";
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging page");
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.main);
};
/*
****************************************
*** twinkleunlink.js: Unlink module
****************************************
* Mode of invocation: Tab ("Unlink")
* Active on: Non-special pages
* Config directives in: TwinkleConfig
*/
Twinkle.unlink = function twinkleunlink() {
if( mw.config.get('wgNamespaceNumber') < 0 ) {
return;
}
twAddPortletLink( Twinkle.unlink.callback, "Unlink", "tw-unlink", "Unlink backlinks" );
};
Twinkle.unlink.getChecked2 = function twinkleunlinkGetChecked2( nodelist ) {
if( !( nodelist instanceof NodeList ) && !( nodelist instanceof HTMLCollection ) ) {
return nodelist.checked ? [ nodelist.values ] : [];
}
var result = [];
for(var i = 0; i < nodelist.length; ++i ) {
if( nodelist[i].checked ) {
result.push( nodelist[i].values );
}
}
return result;
};
// the parameter is used when invoking unlink from admin speedy
Twinkle.unlink.callback = function(presetReason) {
var Window = new Morebits.simpleWindow( 800, 400 );
Window.setTitle( "Unlink backlinks" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#unlink" );
var form = new Morebits.quickForm( Twinkle.unlink.callback.evaluate );
form.append( {
type: 'textarea',
name: 'reason',
label: 'Reason: ',
value: (presetReason ? presetReason : '')
} );
var query;
if(mw.config.get('wgNamespaceNumber') === 6) { // File:
query = {
'action': 'query',
'list': [ 'backlinks', 'imageusage' ],
'bltitle': mw.config.get('wgPageName'),
'iutitle': mw.config.get('wgPageName'),
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'iulimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages.
};
} else {
query = {
'action': 'query',
'list': 'backlinks',
'bltitle': mw.config.get('wgPageName'),
'blfilterredir': 'nonredirects',
'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops
'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages.
};
}
var wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.unlink.callbacks.display.backlinks );
wikipedia_api.params = { form: form, Window: Window, image: mw.config.get('wgNamespaceNumber') === 6 };
wikipedia_api.post();
var root = document.createElement( 'div' );
root.style.padding = '15px'; // just so it doesn't look broken
Morebits.status.init( root );
wikipedia_api.statelem.status( "loading..." );
Window.setContent( root );
Window.display();
};
Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
Twinkle.unlink.backlinksdone = 0;
Twinkle.unlink.imageusagedone = 0;
function processunlink(pages, imageusage) {
var statusIndicator = new Morebits.status((imageusage ? 'Unlinking instances of file usage' : 'Unlinking backlinks'), '0%');
var total = pages.length; // removing doubling of this number - no apparent reason for it
Morebits.wiki.addCheckpoint();
if( !pages.length ) {
statusIndicator.info( '100% (completed)' );
Morebits.wiki.removeCheckpoint();
return;
}
// get an edit token
var params = { reason: reason, imageusage: imageusage, globalstatus: statusIndicator, current: 0, total: total };
for (var i = 0; i < pages.length; ++i)
{
var myparams = $.extend({}, params);
var articlepage = new Morebits.wiki.page(pages[i], 'Unlinking in article "' + pages[i] + '"');
articlepage.setCallbackParameters(myparams);
articlepage.load(imageusage ? Twinkle.unlink.callbacks.unlinkImageInstances : Twinkle.unlink.callbacks.unlinkBacklinks);
}
}
var reason = event.target.reason.value;
var backlinks, imageusage;
if( event.target.backlinks ) {
backlinks = Twinkle.unlink.getChecked2(event.target.backlinks);
}
if( event.target.imageusage ) {
imageusage = Twinkle.unlink.getChecked2(event.target.imageusage);
}
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( event.target );
Morebits.wiki.addCheckpoint();
if (backlinks) {
processunlink(backlinks, false);
}
if (imageusage) {
processunlink(imageusage, true);
}
Morebits.wiki.removeCheckpoint();
};
Twinkle.unlink.backlinksdone = 0;
Twinkle.unlink.imageusagedone = 0;
Twinkle.unlink.callbacks = {
display: {
backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) {
var xmlDoc = apiobj.responseXML;
var havecontent = false;
var list, namespaces, i;
if( apiobj.params.image ) {
var imageusage = $(xmlDoc).find('query imageusage iu');
list = [];
for ( i = 0; i < imageusage.length; ++i ) {
var usagetitle = imageusage[i].getAttribute('title');
list.push( { label: usagetitle, value: usagetitle, checked: true } );
}
if (!list.length)
{
apiobj.params.form.append( { type: 'div', label: 'No instances of file usage found.' } );
}
else
{
apiobj.params.form.append( { type:'header', label: 'File usage' } );
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) {
namespaces.push(Morebits.wikipedia.namespacesFriendly[v]);
});
apiobj.params.form.append( {
type: 'div',
label: "Selected namespaces: " + namespaces.join(', '),
tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]"
});
if ($(xmlDoc).find('query-continue').length) {
apiobj.params.form.append( {
type: 'div',
label: "First " + list.length.toString() + " file usages shown."
});
}
apiobj.params.form.append( {
type: 'checkbox',
name: 'imageusage',
list: list
} );
havecontent = true;
}
}
var backlinks = $(xmlDoc).find('query backlinks bl');
if( backlinks.length > 0 ) {
list = [];
for ( i = 0; i < backlinks.length; ++i ) {
var title = backlinks[i].getAttribute('title');
list.push( { label: title, value: title, checked: true } );
}
apiobj.params.form.append( { type:'header', label: 'Backlinks' } );
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) {
namespaces.push(Morebits.wikipedia.namespacesFriendly[v]);
});
apiobj.params.form.append( {
type: 'div',
label: "Selected namespaces: " + namespaces.join(', '),
tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]"
});
if ($(xmlDoc).find('query-continue').length) {
apiobj.params.form.append( {
type: 'div',
label: "First " + list.length.toString() + " backlinks shown."
});
}
apiobj.params.form.append( {
type: 'checkbox',
name: 'backlinks',
list: list
});
havecontent = true;
}
else
{
apiobj.params.form.append( { type: 'div', label: 'No backlinks found.' } );
}
if (havecontent) {
apiobj.params.form.append( { type:'submit' } );
}
var result = apiobj.params.form.render();
apiobj.params.Window.setContent( result );
}
},
unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) {
var text, oldtext;
text = oldtext = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var wikiPage = new Morebits.wikitext.page(text);
wikiPage.removeLink(mw.config.get('wgPageName'));
text = wikiPage.getText();
if (text === oldtext) {
// Nothing to do, return
Twinkle.unlink.callbacks.success(pageobj);
Morebits.wiki.actionCompleted();
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Removing link(s) to \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.unlink.callbacks.success);
},
unlinkImageInstances: function twinkleunlinkCallbackUnlinkImageInstances(pageobj) {
var text, oldtext;
text = oldtext = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var wikiPage = new Morebits.wikitext.page(text);
wikiPage.commentOutImage(mw.config.get('wgTitle'), 'Commented out');
text = wikiPage.getText();
if (text === oldtext) {
// Nothing to do, return
Twinkle.unlink.callbacks.success(pageobj);
Morebits.wiki.actionCompleted();
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Commenting out use(s) of file \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd'));
pageobj.setCreateOption('nocreate');
pageobj.save(Twinkle.unlink.callbacks.success);
},
success: function twinkleunlinkCallbackSuccess(pageobj) {
var params = pageobj.getCallbackParameters();
var total = params.total;
var now = parseInt( 100 * (params.imageusage ? ++(Twinkle.unlink.imageusagedone) : ++(Twinkle.unlink.backlinksdone))/total, 10 ) + '%';
params.globalstatus.update( now );
if((params.imageusage ? Twinkle.unlink.imageusagedone : Twinkle.unlink.backlinksdone) >= total) {
params.globalstatus.info( now + ' (completed)' );
Morebits.wiki.removeCheckpoint();
}
}
};
/*
****************************************
*** twinklewarn.js: Warn module
****************************************
* Mode of invocation: Tab ("Warn")
* Active on: User talk pages
* Config directives in: TwinkleConfig
*/
Twinkle.warn = function twinklewarn() {
if( mw.config.get('wgNamespaceNumber') === 3 ) {
//twAddPortletLink( Twinkle.warn.callback, "Warn", "tw-warn", "Warn/notify user" );
}
// modify URL of talk page on rollback success pages
if( mw.config.get('wgAction') === 'rollback' ) {
var $vandalTalkLink = $("#mw-rollback-success").find(".mw-usertoollinks a").first();
$vandalTalkLink.css("font-weight", "bold");
$vandalTalkLink.wrapInner($("<span/>").attr("title", "If appropriate, you can use Twinkle to warn the user about their edits to this page."));
var extraParam = "vanarticle=" + mw.util.rawurlencode(mw.config.get("wgPageName").replace(/_/g, " "));
var href = $vandalTalkLink.attr("href");
if (href.indexOf("?") === -1) {
$vandalTalkLink.attr("href", href + "?" + extraParam);
} else {
$vandalTalkLink.attr("href", href + "&" + extraParam);
}
}
};
Twinkle.warn.callback = function twinklewarnCallback() {
if ( !twinkleUserAuthorized ) {
alert("Your account is too new to use Twinkle.");
return;
}
if( mw.config.get('wgTitle').split( '/' )[0] === mw.config.get('wgUserName') &&
!confirm( 'Warning yourself can be seen as a sign of mental instability! Are you sure you want to proceed?' ) ) {
return;
}
var Window = new Morebits.simpleWindow( 600, 440 );
Window.setTitle( "Warn/notify user" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "User talk page warnings", "Template:User_talk_page_warnings#Warnings_and_notices" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#warn" );
var form = new Morebits.quickForm( Twinkle.warn.callback.evaluate );
var main_select = form.append( {
type:'field',
label:'Choose type of warning/notice to issue',
tooltip:'First choose a main warning group, then the specific warning to issue.'
} );
var main_group = main_select.append( {
type:'select',
name:'main_group',
event:Twinkle.warn.callback.change_category
} );
var defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10);
main_group.append( { type:'option', label:'General note (1)', value:'level1', selected: ( defaultGroup === 1 || defaultGroup < 1 || ( Morebits.userIsInGroup( 'sysop' ) ? defaultGroup > 8 : defaultGroup > 7 ) ) } );
main_group.append( { type:'option', label:'Caution (2)', value:'level2', selected: ( defaultGroup === 2 ) } );
main_group.append( { type:'option', label:'Warning (3)', value:'level3', selected: ( defaultGroup === 3 ) } );
main_group.append( { type:'option', label:'Final warning (4)', value:'level4', selected: ( defaultGroup === 4 ) } );
main_group.append( { type:'option', label:'Only warning (4im)', value:'level4im', selected: ( defaultGroup === 5 ) } );
main_group.append( { type:'option', label:'Single issue notices', value:'singlenotice', selected: ( defaultGroup === 6 ) } );
main_group.append( { type:'option', label:'Single issue warnings', value:'singlewarn', selected: ( defaultGroup === 7 ) } );
if( Morebits.userIsInGroup( 'sysop' ) ) {
main_group.append( { type:'option', label:'Blocking', value:'block', selected: ( defaultGroup === 8 ) } );
}
main_select.append( { type:'select', name:'sub_group', event:Twinkle.warn.callback.change_subcategory } ); //Will be empty to begin with.
form.append( {
type:'input',
name:'article',
label:'Linked article',
value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ),
tooltip:'An article can be linked within the notice, perhaps because it was a revert to said article that dispatched this notice. Leave empty for no article to be linked.'
} );
var more = form.append( { type: 'field', name: 'reasonGroup', label: 'Warning information' } );
more.append( { type:'textarea', label:'Optional message:', name:'reason', tooltip:'Perhaps a reason, or that a more detailed notice must be appended' } );
var previewlink = document.createElement( 'a' );
$(previewlink).click(function(){
Twinkle.warn.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = "pointer";
previewlink.textContent = 'Preview';
more.append( { type: 'div', id: 'warningpreview', label: [ previewlink ] } );
more.append( { type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' } );
more.append( { type:'submit', label:'Submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
result.main_group.root = result;
result.previewer = new Morebits.wiki.preview($(result).find('div#twinklewarn-previewbox').last()[0]);
// We must init the first choice (General Note);
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.main_group.dispatchEvent( evt );
};
// This is all the messages that might be dispatched by the code
// Each of the individual templates require the following information:
// label (required): A short description displayed in the dialog
// summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with ". $summaryAd"
// suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such.
Twinkle.warn.messages = {
level1: {
"uw-vandalism1": {
label:"Vandalism",
summary:"General note: Unhelpful changes"
},
"uw-test1": {
label:"Editing tests",
summary:"General note: Editing tests"
},
"uw-delete1": {
label:"Removal of content, blanking",
summary:"General note: Removal of content, blanking"
},
"uw-create1": {
label:"Creating inappropriate pages",
summary:"General note: Creating inappropriate pages"
},
"uw-advert1": {
label:"Using Wikipedia for advertising or promotion",
summary:"General note: Using Wikipedia for advertising or promotion"
},
"uw-copyright1": {
label:"Copyright violation",
summary:"General note: Violating copyright"
},
"uw-error1": {
label:"Deliberately adding wrong information",
summary:"General note: Adding wrong information"
},
"uw-biog1": {
label:"Adding unreferenced controversial information about living persons",
summary:"General note: Adding unreferenced controversial information about living persons"
},
"uw-mos1": {
label:"Manual of style",
summary:"General note: Formatting, date, language, etc (Manual of style)"
},
"uw-move1": {
label:"Page moves against naming conventions or consensus",
summary:"General note: Page moves against naming conventions or consensus"
},
"uw-npov1": {
label:"Not adhering to neutral point of view",
summary:"General note: Not adhering to neutral point of view"
},
"uw-tpv1": {
label:"Changing others' talk page comments",
summary:"General note: Changing others' talk page comments"
},
"uw-qd": {
label:"Removing quick-deletion templates",
summary:"General note: Removing quick-deletion templates"
},
"uw-npa1": {
label:"Personal attack directed at another editor",
summary:"General note: Personal attack directed at another editor"
},
"uw-agf1": {
label:"Not assuming good faith",
summary:"General note: Not assuming good faith"
},
"uw-unsourced1": {
label:"Addition of unsourced or improperly cited material",
summary:"General note: Addition of unsourced or improperly cited material"
}
},
level2: {
"uw-vandalism2": {
label:"Vandalism",
summary:"Caution: Vandalism"
},
"uw-test2": {
label:"Editing tests",
summary:"Caution: Editing tests"
},
"uw-delete2": {
label:"Removal of content, blanking",
summary:"Caution: Removal of content, blanking"
},
"uw-create2": {
label:"Creating inappropriate pages",
summary:"Caution: Creating inappropriate pages"
},
"uw-advert2": {
label:"Using Wikipedia for advertising or promotion",
summary:"Caution: Using Wikipedia for advertising or promotion"
},
"uw-copyright2": {
label:"Copyright violation",
summary:"Caution: Violating copyright"
},
"uw-npov2": {
label:"Not adhering to neutral point of view",
summary:"Caution: Not adhering to neutral point of view"
},
"uw-error2": {
label:"Deliberately adding wrong information",
summary:"Caution: Adding wrong information"
},
"uw-biog2": {
label:"Adding unreferenced controversial information about living persons",
summary:"Caution: Adding unreferenced controversial information about living persons"
},
"uw-mos2": {
label:"Manual of style",
summary:"Caution: Formatting, date, language, etc (Manual of style)"
},
"uw-move2": {
label:"Page moves against naming conventions or consensus",
summary:"Caution: Page moves against naming conventions or consensus"
},
"uw-tpv2": {
label:"Changing others' talk page comments",
summary:"Caution: Changing others' talk page comments"
},
"uw-npa2": {
label:"Personal attack directed at another editor",
summary:"Caution: Personal attack directed at another editor"
},
"uw-agf2": {
label:"Not assuming good faith",
summary:"Caution: Not assuming good faith"
},
"uw-unsourced2": {
label:"Addition of unsourced or improperly cited material",
summary:"Caution: Addition of unsourced or improperly cited material"
}
},
level3: {
"uw-vandalism3": {
label:"Vandalism",
summary:"Warning: Vandalism"
},
"uw-test3": {
label:"Editing tests",
summary:"Warning: Editing tests"
},
"uw-delete3": {
label:"Removal of content, blanking",
summary:"Warning: Removal of content, blanking"
},
"uw-create3": {
label:"Creating inappropriate pages",
summary:"Warning: Creating inappropriate pages"
},
"uw-advert3": {
label:"Using Wikipedia for advertising or promotion",
summary:"Warning: Using Wikipedia for advertising or promotion"
},
"uw-npov3": {
label:"Not adhering to neutral point of view",
summary:"Warning: Not adhering to neutral point of view"
},
"uw-error3": {
label:"Deliberately adding wrong information",
summary:"Warning: Adding wrong information"
},
"uw-biog3": {
label:"Adding unreferenced controversial or defamatory information about living persons",
summary:"Warning: Adding unreferenced controversial information about living persons"
},
"uw-mos3": {
label:"Manual of style",
summary:"Warning: Formatting, date, language, etc (Manual of style)"
},
"uw-move3": {
label:"Page moves against naming conventions or consensus",
summary:"Warning: Page moves against naming conventions or consensus"
},
"uw-tpv3": {
label:"Changing others' talk page comments",
summary:"Warning: Changing others' talk page comments"
},
"uw-npa3": {
label:"Personal attack directed at another editor",
summary:"Warning: Personal attack directed at another editor"
},
"uw-agf3": {
label:"Not assuming good faith",
summary:"Warning: Not assuming good faith"
}
},
level4: {
"uw-generic4": {
label:"Generic warning (for template series missing level 4)",
summary:"Final warning notice"
},
"uw-vandalism4": {
label:"Vandalism",
summary:"Final warning: Vandalism"
},
"uw-test4": {
label:"Editing tests",
summary:"Final warning: Editing tests"
},
"uw-delete4": {
label:"Removal of content, blanking",
summary:"Final warning: Removal of content, blanking"
},
"uw-create4": {
label:"Creating inappropriate pages",
summary:"Final warning: Creating inappropriate pages"
},
"uw-advert4": {
label:"Using Wikipedia for advertising or promotion",
summary:"Final warning: Using Wikipedia for advertising or promotion"
},
"uw-npov4": {
label:"Not adhering to neutral point of view",
summary:"Final warning: Not adhering to neutral point of view"
},
"uw-error4": {
label:"Deliberately adding wrong information",
summary:"Final Warning: Adding wrong information"
},
"uw-biog4": {
label:"Adding unreferenced defamatory information about living persons",
summary:"Final warning: Adding unreferenced controversial information about living persons"
},
"uw-mos4": {
label:"Manual of style",
summary:"Final warning: Formatting, date, language, etc (Manual of style)"
},
"uw-move4": {
label:"Page moves against naming conventions or consensus",
summary:"Final warning: Page moves against naming conventions or consensus"
},
"uw-npa4": {
label:"Personal attack directed at another editor",
summary:"Final warning: Personal attack directed at another editor"
}
},
level4im: {
"uw-vandalism4im": {
label:"Vandalism",
summary:"Only warning: Vandalism"
},
"uw-delete4im": {
label:"Removal of content, blanking",
summary:"Only warning: Removal of content, blanking"
},
"uw-create4im": {
label:"Creating inappropriate pages",
summary:"Only warning: Creating inappropriate pages"
},
"uw-biog4im": {
label:"Adding unreferenced defamatory information about living persons",
summary:"Only warning: Adding unreferenced controversial information about living persons"
},
"uw-move4im": {
label:"Page moves against naming conventions or consensus",
summary:"Only warning: Page moves against naming conventions or consensus"
},
"uw-npa4im": {
label:"Personal attack directed at another editor",
summary:"Only warning: Personal attack directed at another editor"
}
},
singlenotice: {
"uw-badcat": {
label:"Adding incorrect categories",
summary:"Notice: Adding incorrect categories"
},
"uw-bite": {
label:"\"Biting\" newcomers",
summary:"Notice: \"Biting\" newcomers"
},
"uw-coi": {
label:"Possible conflict of interest",
summary:"Notice: Possible conflict of interest"
},
"uw-encopypaste": {
label:"Direct copying of article from English Wikipedia",
summary:"Notice: Direct copying of article from English Wikipedia"
},
"uw-encopyright": {
label:"Not giving attribution for content from another Wikipedia",
summary:"Notice: Reusing content from English Wikipedia without attribution"
},
"uw-emptycat": {
label:"Category created does not contain enough pages",
summary:"Notice: Creating empty categories"
},
"uw-joke": {
label:"Using improper humor",
summary:"Notice: Using improper humor"
},
"uw-lang": {
label:"Changing between types of English without a good reason",
summary:"Notice: Unnecessarily changing between British and American English"
},
"uw-newarticle": {
label:"Tips on creating new articles",
summary:"Notice: How to make your articles better"
},
"uw-notenglish": {
label:"Changes not in English",
summary:"Notice: Please edit in English"
},
"uw-otherweb": {
label:"Use \"Other websites\", not \"External links\"",
summary:"Notice: Use \"Other websites\", not \"External links\""
},
"uw-sandbox": {
label:"Removing the sandbox header",
summary:"Notice: Do not remove sandbox header"
},
"uw-selfrevert": {
label:"Undoing recent test",
summary:"Notice: Undoing recent test"
},
"uw-simple": {
label:"Not making changes in simple English",
summary:"Notice: Not making changes in simple English"
},
"uw-spellcheck": {
label:"Review spelling, etc.",
summary:"Notice: Review spelling, etc."
},
"uw-subst": {
label:"Remember to subst: templates",
summary:"Notice: Remember to subst: templates"
},
"uw-tilde": {
label:"Not signing posts",
summary:"Notice: Not signing posts"
},
"uw-upload": {
label:"Image uploads not allowed in Simple English Wikipedia",
summary:"Notice: Image uploads not allowed in Simple English Wikipedia"
},
"uw-warn": {
label:"Use user warn templates",
summary:"Notice: Use user warn templates"
}
},
singlewarn: {
"uw-3rr": {
label:"Edit warring",
summary:"Warning: Involved in edit war"
},
"uw-attack": {
label:"Creating attack pages",
summary:"Warning: Creating attack pages"
},
"uw-cyberbully": {
label:"Cyberbullying",
summary:"Warning: Cyberbullying"
},
"uw-disruption": {
label:"Project disruption",
summary:"Warning: Project disruption"
},
"uw-longterm": {
label:"Long term abuse",
summary:"Warning: Long term abuse"
},
"uw-qd": {
label:"Removing quick deletion templates from articles",
summary:"Warning: Removing quick deletion templates from articles"
},
"uw-spam": {
label:"Adding spam links",
summary:"Warning: Adding spam links"
},
"uw-userpage": {
label:"Userpage or subpage is against policy",
summary:"Warning: Userpage or subpage is against policy"
}
},
block: {
"uw-block1": {
label: "Block level 1",
summary: "You have been temporarily blocked",
reasonParam: true
},
"uw-block2": {
label: "Block level 2",
summary: "You have been blocked",
reasonParam: true
},
"uw-block3": {
label: "Block level 3",
summary: "You have been indefinitely blocked",
reasonParam: true
},
"UsernameBlocked": {
label: "Username block",
summary: "You have been blocked for violation of the [[Wikipedia:Username|username policy]]",
reasonParam: true
},
"UsernameHardBlocked": {
label: "Username hard block",
summary: "You have been blocked for a blatant violation of the [[Wikipedia:Username|username policy]]",
reasonParam: true
},
"Blocked proxy": {
label: "Blocked proxy",
summary: "You have been blocked because this IP is an [[open proxy]]"
},
"Uw-spamblock": {
label: "Spam block",
summary: "You have been blocked for [[Wikipedia:Spam|advertising or promotion]]"
},
"Cyberbully block": {
label: "Cyberbully block",
summary: "You have been blocked for [[Wikipedia:Cyberbullying|cyberbullying]]"
},
"Talkpage-revoked": {
label: "Talk-page access removed",
summary: "Your ability to change this [[Wikipedia:Talk page|talk page]] has been removed"
}
}
};
Twinkle.warn.prev_block_timer = null;
Twinkle.warn.prev_block_reason = null;
Twinkle.warn.prev_article = null;
Twinkle.warn.prev_reason = null;
Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) {
var value = e.target.value;
var sub_group = e.target.root.sub_group;
var messages = Twinkle.warn.messages[ value ];
sub_group.main_group = value;
var old_subvalue = sub_group.value;
var old_subvalue_re;
if( old_subvalue ) {
old_subvalue = old_subvalue.replace(/\d*(im)?$/, '' );
old_subvalue_re = new RegExp( RegExp.escape( old_subvalue ) + "(\\d*(?:im)?)$" );
}
while( sub_group.hasChildNodes() ){
sub_group.removeChild( sub_group.firstChild );
}
for( var i in messages ) {
var selected = false;
if( old_subvalue && old_subvalue_re.test( i ) ) {
selected = true;
}
var elem = new Morebits.quickForm.element( { type:'option', label:"{{" + i + "}}: " + messages[i].label, value:i, selected: selected } );
sub_group.appendChild( elem.render() );
}
if( value === 'block' ) {
// create the block-related fields
var more = new Morebits.quickForm.element( { type: 'div', id: 'block_fields' } );
more.append( {
type: 'input',
name: 'block_timer',
label: 'Period of blocking / Host ',
tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc... If you selected "blocked proxy", this text box will append the host name of the server'
} );
more.append( {
type: 'input',
name: 'block_reason',
label: '"You have been blocked for ..." ',
tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.'
} );
e.target.root.insertBefore( more.render(), e.target.root.lastChild );
// restore saved values of fields
if(Twinkle.warn.prev_block_timer !== null) {
e.target.root.block_timer.value = Twinkle.warn.prev_block_timer;
Twinkle.warn.prev_block_timer = null;
}
if(Twinkle.warn.prev_block_reason !== null) {
e.target.root.block_reason.value = Twinkle.warn.prev_block_reason;
Twinkle.warn.prev_block_reason = null;
}
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.root.article.value;
}
e.target.root.article.disabled = false;
$(e.target.root.reason).parent().hide();
e.target.root.previewer.closePreview();
} else if( e.target.root.block_timer ) {
// hide the block-related fields
if(!e.target.root.block_timer.disabled && Twinkle.warn.prev_block_timer === null) {
Twinkle.warn.prev_block_timer = e.target.root.block_timer.value;
}
if(!e.target.root.block_reason.disabled && Twinkle.warn.prev_block_reason === null) {
Twinkle.warn.prev_block_reason = e.target.root.block_reason.value;
}
$(e.target.root).find("#block_fields").remove();
if(e.target.root.article.disabled && Twinkle.warn.prev_article !== null) {
e.target.root.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.root.article.disabled = false;
$(e.target.root.reason).parent().show();
e.target.root.previewer.closePreview();
}
// clear overridden label on article textbox
Morebits.quickForm.setElementTooltipVisibility(e.target.root.article, true);
Morebits.quickForm.resetElementLabel(e.target.root.article);
};
Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) {
var main_group = e.target.form.main_group.value;
var value = e.target.form.sub_group.value;
if( main_group === 'singlewarn' ) {
if( value === 'uw-username' ) {
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.notArticle = true;
e.target.form.article.value = '';
} else if( e.target.form.article.notArticle ) {
if(Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.notArticle = false;
}
} else if( main_group === 'block' ) {
if( Twinkle.warn.messages.block[value].indefinite ) {
if(Twinkle.warn.prev_block_timer === null) {
Twinkle.warn.prev_block_timer = e.target.form.block_timer.value;
}
e.target.form.block_timer.disabled = true;
e.target.form.block_timer.value = 'indefinite';
} else if( e.target.form.block_timer.disabled ) {
if(Twinkle.warn.prev_block_timer !== null) {
e.target.form.block_timer.value = Twinkle.warn.prev_block_timer;
Twinkle.warn.prev_block_timer = null;
}
e.target.form.block_timer.disabled = false;
}
if( Twinkle.warn.messages.block[value].pageParam ) {
if(Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.disabled = false;
} else if( !e.target.form.article.disabled ) {
if(Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.disabled = true;
e.target.form.article.value = '';
}
if( Twinkle.warn.messages.block[value].reasonParam ) {
if(Twinkle.warn.prev_block_reason !== null) {
e.target.form.block_reason.value = Twinkle.warn.prev_block_reason;
Twinkle.warn.prev_block_reason = null;
}
e.target.form.block_reason.disabled = false;
} else if( !e.target.form.block_reason.disabled ) {
if(Twinkle.warn.prev_block_reason === null) {
Twinkle.warn.prev_block_reason = e.target.form.block_reason.value;
}
e.target.form.block_reason.disabled = true;
e.target.form.block_reason.value = '';
}
}
// change form labels according to the warning selected
if (value === "uw-username") {
Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, false);
Morebits.quickForm.overrideElementLabel(e.target.form.article, "Username violates policy because... ");
} else {
Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, true);
Morebits.quickForm.resetElementLabel(e.target.form.article);
}
};
Twinkle.warn.callbacks = {
preview: function(form) {
var templatename = form.sub_group.value;
var templatetext = '{{subst:' + templatename;
var linkedarticle = form.article.value;
if (templatename in Twinkle.warn.messages.block) {
if( linkedarticle && Twinkle.warn.messages.block[templatename].pageParam ) {
templatetext += '|page=' + linkedarticle;
}
var blocktime = form.block_timer.value;
if( /te?mp|^\s*$|min/.exec( blocktime ) || Twinkle.warn.messages.block[templatename].indefinite ) {
; // nothing
} else if( /indef|\*|max/.exec( blocktime ) ) {
templatetext += '|indef=yes';
} else {
templatetext += '|host=' + blocktime;
templatetext += '|time=' + blocktime;
}
var blockreason = form.block_reason.value;
if( blockreason ) {
templatetext += '|reason=' + blockreason;
}
templatetext += "|sig=true}}";
} else {
if (linkedarticle) {
// add linked article for user warnings (non-block templates)
templatetext += '|1=' + linkedarticle;
}
templatetext += '}}';
// add extra message for non-block templates
var reason = form.reason.value;
if (reason) {
templatetext += " ''" + reason + "''";
}
}
form.previewer.beginRender(templatetext);
},
main: function( pageobj ) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var messageData = Twinkle.warn.messages[params.main_group][params.sub_group];
var history_re = /<!-- Template:(uw-.*?) -->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4}) \(UTC\)/g;
var history = {};
var latest = { date:new Date( 0 ), type:'' };
var current;
while( ( current = history_re.exec( text ) ) ) {
var current_date = new Date( current[2] + ' UTC' );
if( !( current[1] in history ) || history[ current[1] ] < current_date ) {
history[ current[1] ] = current_date;
}
if( current_date > latest.date ) {
latest.date = current_date;
latest.type = current[1];
}
}
var date = new Date();
if( params.sub_group in history ) {
var temp_time = new Date( history[ params.sub_group ] );
temp_time.setUTCHours( temp_time.getUTCHours() + 24 );
if( temp_time > date ) {
if( !confirm( "An identical " + params.sub_group + " has been issued in the last 24 hours. \nWould you still like to add this warning/notice?" ) ) {
pageobj.statelem.info( 'aborted per user request' );
return;
}
}
}
latest.date.setUTCMinutes( latest.date.getUTCMinutes() + 1 ); // after long debate, one minute is max
if( latest.date > date ) {
if( !confirm( "A " + latest.type + " has been issued in the last minute. \nWould you still like to add this warning/notice?" ) ) {
pageobj.statelem.info( 'aborted per user request' );
return;
}
}
var mainheaderRe = new RegExp("==+\\s*Warnings\\s*==+");
var headerRe = new RegExp( "^==+\\s*(?:" + date.getUTCMonthName() + '|' + date.getUTCMonthNameAbbrev() + ")\\s+" + date.getUTCFullYear() + "\\s*==+", 'm' );
if( text.length > 0 ) {
text += "\n\n";
}
if( params.main_group === 'block' ) {
var article = '', reason = '', host = '', time = null;
if( Twinkle.getPref('blankTalkpageOnIndefBlock') && params.sub_group !== 'uw-lblock' && ( Twinkle.warn.messages.block[params.sub_group].indefinite || (/indef|\*|max/).exec( params.block_timer ) ) ) {
Morebits.status.info( 'Info', 'Blanking talk page per preferences and creating a new level 2 heading for the date' );
text = "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
} else if( !headerRe.exec( text ) ) {
Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' );
text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
}
if( params.reason && Twinkle.warn.messages.block[params.sub_group].reasonParam ) {
reason = '|reason=' + params.reason;
}
if( /te?mp|^\s*$|min/.exec( params.block_timer ) || Twinkle.warn.messages.block[params.sub_group].indefinite ) {
time = '';
} else if( /indef|\*|max/.exec( params.block_timer ) ) {
time = '|indef=yes';
} else {
time = '|time=' + params.block_timer;
}
if ( params.sub_group === "Blocked proxy" )
{
text += "{{" + params.sub_group + "|host=" + params.block_timer + "}}";
} else {
text += "{{subst:" + params.sub_group + time + reason + "|sig=yes}}";
}
} else {
if( !headerRe.exec( text ) ) {
Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' );
text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n";
}
text += "{{subst:" + params.sub_group + ( params.article ? '|1=' + params.article : '' ) + "|subst=subst:}}" + (params.reason ? " ''" + params.reason + "'' ": ' ' ) + "~~~~";
}
if ( Twinkle.getPref('showSharedIPNotice') && Morebits.isIPAddress( mw.config.get('wgTitle') ) ) {
Morebits.status.info( 'Info', 'Adding a shared IP notice' );
text += "\n{{subst:SharedIPAdvice}}";
}
var summary = messageData.summary;
if ( messageData.suppressArticleInSummary !== true && params.article ) {
summary += " on [[" + params.article + "]]";
}
summary += "." + Twinkle.getPref("summaryAd");
pageobj.setPageText( text );
pageobj.setEditSummary( summary );
pageobj.setWatchlist( Twinkle.getPref('watchWarnings') );
pageobj.save();
}
};
Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) {
// First, check to make sure a reason was filled in if uw-username was selected
if(e.target.sub_group.value === 'uw-username' && e.target.article.value.trim() === '') {
alert("You must supply a reason for the {{uw-username}} template.");
return;
}
// Then, grab all the values provided by the form
var params = {
reason: e.target.block_reason ? e.target.block_reason.value : e.target.reason.value,
main_group: e.target.main_group.value,
sub_group: e.target.sub_group.value,
article: e.target.article.value, // .replace( /^(Image|Category):/i, ':$1:' ), -- apparently no longer needed...
block_timer: e.target.block_timer ? e.target.block_timer.value : null
};
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = "Warning complete, reloading talk page in a few seconds";
var wikipedia_page = new Morebits.wiki.page( mw.config.get('wgPageName'), 'User talk page modification' );
wikipedia_page.setCallbackParameters( params );
wikipedia_page.setFollowRedirect( true );
wikipedia_page.load( Twinkle.warn.callbacks.main );
};
/*
****************************************
*** twinklexfd.js: XFD module
****************************************
* Mode of invocation: Tab ("XFD")
* Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects
* Config directives in: TwinkleConfig
*/
Twinkle.xfd = function twinklexfd() {
// Disable on:
// * special pages
// * non-existent pages
// * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2)
// * file pages without actual files (these are eligible for CSD G8)
if ( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && (document.getElementById('mw-sharedupload') || (!document.getElementById('mw-imagepage-section-filehistory') && !Morebits.wiki.isPageRedirect()))) ) {
return;
}
//twAddPortletLink( Twinkle.xfd.callback, "RfD", "tw-xfd", "Nominate for deletion" );
};
Twinkle.xfd.num2order = function twinklexfdNum2order( num ) {
switch( num ) {
case 1: return '';
case 2: return '2nd';
case 3: return '3rd';
default: return num + 'th';
}
};
Twinkle.xfd.currentRationale = null;
// error callback on Morebits.status.object
Twinkle.xfd.printRationale = function twinklexfdPrintRationale() {
if (Twinkle.xfd.currentRationale) {
var p = document.createElement("p");
p.textContent = "Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:";
var pre = document.createElement("pre");
pre.className = "toccolours";
pre.style.marginTop = "0";
pre.textContent = Twinkle.xfd.currentRationale;
p.appendChild(pre);
Morebits.status.root.appendChild(p);
// only need to print the rationale once
Twinkle.xfd.currentRationale = null;
}
};
Twinkle.xfd.callback = function twinklexfdCallback() {
if (!twinkleUserAuthorized) {
alert("Your account is too new to use Twinkle.");
return;
}
var Window = new Morebits.simpleWindow( 600, 350 );
Window.setTitle( "Nominate for deletion (RfD)" );
Window.setScriptName( "Twinkle" );
Window.addFooterLink( "Deletion policy", "Wikipedia:Deletion policy" );
Window.addFooterLink( "About deletion discussions", "WP:RfD" );
Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#xfd" );
var form = new Morebits.quickForm( Twinkle.xfd.callback.evaluate );
var categories = form.append( {
type: 'select',
name: 'category',
label: 'Select wanted type of category: ',
tooltip: 'This default should be the most appropriate, as no other deletion discussion pages exist here.',
event: Twinkle.xfd.callback.change_category
} );
categories.append( {
type: 'option',
label: 'RfD (Requests for deletion)',
selected: true,
value: 'afd'
} );
form.append( {
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notify',
tooltip: "A notification template will be placed on the creator's talk page if this is true.",
checked: true
}
]
}
);
form.append( {
type: 'field',
label:'Work area',
name: 'work_area'
} );
form.append( { type:'submit' } );
var result = form.render();
Window.setContent( result );
Window.display();
// We must init the controls
var evt = document.createEvent( "Event" );
evt.initEvent( 'change', true, true );
result.category.dispatchEvent( evt );
};
Twinkle.xfd.previousNotify = true;
Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) {
var value = e.target.value;
var form = e.target.form;
var old_area = Morebits.quickForm.getElements(e.target.form, "work_area")[0];
var work_area = null;
var oldreasontextbox = form.getElementsByTagName('textarea')[0];
var oldreason = (oldreasontextbox ? oldreasontextbox.value : '');
work_area = new Morebits.quickForm.element( {
type: 'field',
label: 'Requests for deletion',
name: 'work_area'
} );
work_area.append( {
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. This option is not normally required.'
}
]
} );
work_area.append( {
type: 'textarea',
name: 'xfdreason',
label: 'Reason: '
} );
work_area = work_area.render();
old_area.parentNode.replaceChild( work_area, old_area );
}
Twinkle.xfd.callbacks = {
afd: {
main: function(apiobj) {
var xmlDoc = apiobj.responseXML;
var titles = $(xmlDoc).find('allpages p');
// There has been no earlier entries with this prefix, just go on.
if( titles.length <= 0 ) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
var number = 0;
for( var i = 0; i < titles.length; ++i ) {
var title = titles[i].getAttribute('title');
// First, simple test, is there an instance with this exact name?
if( title === 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') ) {
number = Math.max( number, 1 );
continue;
}
var order_re = new RegExp( '^' +
RegExp.escape( 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), true ) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
var match = order_re.exec( title );
// No match; A non-good value
if( !match ) {
continue;
}
// A match, set number to the max of current
number = Math.max( number, Number(match[1]) );
}
apiobj.params.number = Twinkle.xfd.num2order( parseInt( number, 10 ) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + apiobj.params.numbering;
Morebits.status.info( "Next discussion page", "[[" + apiobj.params.discussionpage + "]]" );
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to the discussion page";
// Tagging article
var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Adding deletion tag to article");
if(window.location.search.includes("redirect=no")) {
wikipedia_page.setFollowRedirect(false); // User's intention was probably to tag the redirect itself
} else {
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
}
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle);
},
// Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page
taggingArticle: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
// Check for existing AfD tag, for the benefit of new page patrollers
var textNoAfd = text.replace(/\{\{\s*(Requests for deletion\/dated|RfDM)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, "");
if (text !== textNoAfd) {
if (confirm("An RfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current RfD tag (not recommended), or Cancel to abandon your nomination.")) {
text = textNoAfd;
} else {
statelem.error("Article already tagged with RfD tag, and you chose to abort");
window.location.reload();
return;
}
}
// Now we know we want to go ahead with it, trigger the other AJAX requests
// Starting discussion page
var wikipedia_page = new Morebits.wiki.page(params.discussionpage, "Creating article deletion discussion page");
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage);
// Today's list
var date = new Date();
wikipedia_page = new Morebits.wiki.page('Wikipedia:Requests for deletion', "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList);
// Notification to first contributor
if (params.usertalk) {
var thispage = new Morebits.wiki.page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.lookupCreator(Twinkle.xfd.callbacks.afd.userNotification);
}
// Then, test if there are speedy deletion-related templates on the article.
var textNoSd = text.replace(/\{\{\s*(db(-\w*)?|qd|delete|(?:hang|hold)[\- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, "");
if (text !== textNoSd && confirm("A quick deletion tag was found on this page. Should it be removed?")) {
text = textNoSd;
}
pageobj.setPageText(( params.noinclude ? "<noinclude>" : "" ) + "\{\{RfD|" + params.reason + "\}\}\n" + ( params.noinclude ? "</noinclude>" : "" ) + text);
pageobj.setEditSummary("Nominated for deletion; see [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchPage')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('nocreate');
pageobj.save();
},
discussionPage: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
pageobj.setPageText("{{subst:RfD/Preload/Template|deletereason=" + params.reason + "}}\n");
pageobj.setEditSummary("Creating deletion discussion page for [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchDiscussion')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('createonly');
pageobj.save(function() {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
todaysList: function(pageobj) {
var old_text = pageobj.getPageText() + "\n"; // MW strips trailing blanks, but we like them, so we add a fake one
var params = pageobj.getCallbackParameters();
var statelem = pageobj.getStatusElement();
var text = old_text.replace( /(<\!-- Add new entries to the TOP of the following list -->\n+)/, "$1{{Wikipedia:Requests for deletion/Requests/" + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + params.numbering + "}}\n");
if( text === old_text ) {
statelem.error( 'failed to find target spot for the discussion' );
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary("Adding [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd'));
switch (Twinkle.getPref('xfdWatchList')) {
case 'yes':
pageobj.setWatchlist(true);
break;
case 'no':
pageobj.setWatchlistFromPreferences(false);
break;
default:
pageobj.setWatchlistFromPreferences(true);
break;
}
pageobj.setCreateOption('recreate');
pageobj.save();
},
userNotification: function(pageobj) {
var params = pageobj.getCallbackParameters();
var initialContrib = pageobj.getCreator();
var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")");
var notifytext = "\n{{subst:RFDNote|1=" + mw.config.get('wgPageName') + "|2=" + mw.config.get('wgPageName') + ( params.numbering !== '' ? '|order= ' + params.numbering : '' ) + "}} ~~~~";
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary("Notification: listing at [[WP:RfD|requests for deletion]] of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd'));
usertalkpage.setCreateOption('recreate');
switch (Twinkle.getPref('xfdWatchUser')) {
case 'yes':
usertalkpage.setWatchlist(true);
break;
case 'no':
usertalkpage.setWatchlistFromPreferences(false);
break;
default:
usertalkpage.setWatchlistFromPreferences(true);
break;
}
usertalkpage.setFollowRedirect(true);
usertalkpage.append();
}
}
};
Twinkle.xfd.callback.evaluate = function(e) {
mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country!
var type = e.target.category.value;
var usertalk = e.target.notify.checked;
var reason = e.target.xfdreason.value;
var xfdtarget, xfdtarget2, puf, noinclude, tfdinline, notifyuserspace;
Morebits.simpleWindow.setButtonsEnabled( false );
Morebits.status.init( e.target );
Twinkle.xfd.currentRationale = reason;
Morebits.status.onError(Twinkle.xfd.printRationale);
if( !type ) {
Morebits.status.error( 'Error', 'no action given' );
return;
}
var query, wikipedia_page, wikipedia_api, logpage, params;
var date = new Date();
query = {
'action': 'query',
'list': 'allpages',
'apprefix': 'Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'),
'apnamespace': 4,
'apfilterredir': 'nonredirects',
'aplimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500
};
wikipedia_api = new Morebits.wiki.api( 'Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main );
wikipedia_api.params = { usertalk:usertalk, reason:reason, noinclude:noinclude };
wikipedia_api.post();
};
/**
* General initialization code
*/
var scriptpathbefore = "https://dev.miraheze.org" + mw.util.wikiScript( "index" ) + "?title=",
scriptpathafter = "&action=raw&ctype=text/javascript&happy=yes";
// Retrieve the user's Twinkle preferences
$.ajax({
url: scriptpathbefore + "User:" + encodeURIComponent( mw.config.get("wgUserName")) + "/twinkleoptions.js" + scriptpathafter,
dataType: "text",
error: function () { mw.notify( "Could not load twinkleoptions.js" ); },
success: function ( optionsText ) {
// Quick pass if user has no options
if ( optionsText === "" ) {
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace( /(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, "" );
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if ( optionsText.lastIndexOf( "window.Twinkle.prefs = ", 0 ) === 0 ) {
optionsText = optionsText.replace( /(?:^window.Twinkle.prefs = |;\n*$)/g, "" );
}
try {
var options = JSON.parse( optionsText );
// Assuming that our options evolve, we will want to transform older versions:
//if ( options.optionsVersion === undefined ) {
// ...
// options.optionsVersion = 1;
//}
//if ( options.optionsVersion === 1 ) {
// ...
// options.optionsVersion = 2;
//}
// At the same time, twinkleconfig.js needs to be adapted to write a higher version number into the options.
if ( options ) {
Twinkle.prefs = options;
}
}
catch ( e ) {
mw.notify("Could not parse twinkleoptions.js");
}
},
complete: function () {
$( Twinkle.load );
}
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function () {
// Don't activate on special pages other than "Contributions" so that they load faster, especially the watchlist.
// Also, Twinkle is incompatible with Internet Explorer versions 8 or lower, so don't load there either.
var specialPageWhitelist = [ 'Block', 'Contributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsInGroup('sysop')) {
specialPageWhitelist = specialPageWhitelist.concat([ 'DeletedContributions', 'Prefixindex' ]);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
specialPageWhitelist.indexOf(mw.config.get('wgCanonicalSpecialPageName')) === -1) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
if ($.client.profile().name === 'msie' && $.client.profile().versionNumber < 9) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.api.setApiUserAgent('Twinkle/2.0 (' + mw.config.get('wgDBname') + ')');
// Load the modules in the order that the tabs should appears
// Deletion
Twinkle.speedy();
// Misc. ones last
Twinkle.diff();
Twinkle.unlink();
Twinkle.config.init();
Twinkle.fluff.init();
if ( Morebits.userIsInGroup('sysop') ) {
Twinkle.batchdelete();
Twinkle.batchprotect();
Twinkle.batchundelete();
}
// Run the initialization callbacks for any custom modules
$( Twinkle.initCallbacks ).each(function ( k, v ) { v(); });
Twinkle.addInitCallback = function ( func ) { func(); };
// Increases text size in Twinkle dialogs, if so configured
if ( Twinkle.getPref( "dialogLargeFont" ) ) {
mw.util.addCSS( ".morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } " +
".morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }" );
}
};
} ( window, document, jQuery )); // End wrap with anonymous function
// </nowiki>
3399804aac47c3b253525d584e1077068e37daf4
MediaWiki:Twinkle.js
8
84
161
160
2023-08-28T02:15:26Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
javascript
text/javascript
/* twinkle js file [[Category:Twinkle]]
loads all dependencies then twinkle */
mw.loader.getScript( 'https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript' ).then( function () {mw.loader.load(["mediawiki.util"]);mw.loader.load(["jquery.ui"]);mw.loader.load(["jquery.tipsy"]);mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.css?action=raw&ctype=text/css", "text/css");mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript");}, function ( e ) {mw.log.error( e.message );} );
a49ff2e963c82cee4e45c90883102d1e662bd8b8
Template:Uses TemplateStyles
10
85
163
162
2023-08-28T02:15:26Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
wikitext
text/x-wiki
<includeonly>{{#invoke:Uses TemplateStyles|main}}</includeonly><noinclude>
{{Uses TemplateStyles|Template:Uses TemplateStyles/example.css|nocat=true}}
{{documentation}}
<!-- Categories go on the /doc subpage and interwikis go on Wikidata. -->
</noinclude>
7e26d8f257e302bd8a3dcbe53f52741ae0884f74
Module:Uses TemplateStyles
828
86
165
164
2023-08-28T02:15:27Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
-- This module implements the {{Uses TemplateStyles}} template.
local mMessageBox = require('Module:Message box')
local p = {}
function p.main(frame)
local origArgs = frame:getParent().args
local args = {}
for k, v in pairs(origArgs) do
v = v:match('^%s*(.-)%s*$')
if v ~= '' then
args[k] = v
end
end
return p._main(args)
end
function p._main(args)
return p.renderBox(args)
end
function p.renderBox(tStyles)
local boxArgs = {}
if #tStyles < 1 then
boxArgs.text = '<strong class="error">Error: no TemplateStyles specified</strong>'
else
local tStylesLinks = {}
for i, ts in ipairs(tStyles) do
local sandboxLink = nil
local tsTitle = mw.title.new(ts)
if tsTitle then
local tsSandboxTitle = mw.title.new(string.format('%s:%s/sandbox/%s', tsTitle.nsText, tsTitle.baseText, tsTitle.subpageText))
if tsSandboxTitle and tsSandboxTitle.exists then
sandboxLink = string.format(' ([[:%s|sandbox]])', tsSandboxTitle.prefixedText)
end
end
tStylesLinks[i] = string.format('[[:%s]]%s', ts, sandboxLink or '')
end
local tStylesList = mw.text.listToText(tStylesLinks)
boxArgs.text = 'This ' ..
(mw.title.getCurrentTitle():inNamespaces(828,829) and 'module' or 'template') ..
' uses [[mw:Extension:TemplateStyles|TemplateStyles]]:\n' .. tStylesList
end
boxArgs.type = 'notice'
boxArgs.small = true
boxArgs.image = '[[File:Farm-Fresh css add.svg|32px|alt=CSS]]'
return mMessageBox.main('mbox', boxArgs)
end
return p
3c7364ddaba9beb17a73b0f5256cd7fc3b3051f4
Module:No globals
828
87
167
166
2023-08-28T02:15:27Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
local mt = getmetatable(_G) or {}
function mt.__index (t, k)
if k ~= 'arg' then
error('Tried to read nil global ' .. tostring(k), 2)
end
return nil
end
function mt.__newindex(t, k, v)
if k ~= 'arg' then
error('Tried to write global ' .. tostring(k), 2)
end
rawset(t, k, v)
end
setmetatable(_G, mt)
8ce3969f7d53b08bd00dabe4cc9780bc6afd412a
Module:Reply to
828
88
169
168
2023-08-28T02:15:28Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
local p = {}
local function makeError(msg)
msg ='Error in [[Template:Reply to]]: ' .. msg
return mw.text.tag('strong', {['class']='error'}, msg)
end
function p.replyto(frame)
local origArgs = frame:getParent().args
local args = {}
local maxArg = 1
local usernames = 0
for k, v in pairs(origArgs) do
if type(k) == 'number' then
if mw.ustring.match(v,'%S') then
if k > maxArg then maxArg = k end
usernames = usernames + 1
local title = mw.title.new(v)
if not title then return makeError('Input contains forbidden characters.') end
args[k] = title.rootText
end
elseif v == '' and k:sub(0,5) == 'label' then
args[k] = '​'
else
args[k] = v
end
end
if usernames > (tonumber(frame.args.max) or 50) then
return makeError(string.format(
'More than %s names specified.',
tostring(frame.args.max or 50)
))
else
if usernames < 1 then
if frame.args.example then args[1] = frame.args.example else return makeError('Username not given.') end
end
args['label1'] = args['label1'] or args['label']
local isfirst = true
local outStr = args['prefix'] or '@'
for i = 1, maxArg do
if args[i] then
if isfirst then
isfirst = false
else
if ( (usernames > 2) or ((usernames == 2) and (args['c'] == '')) ) then outStr = outStr..', ' end
if i == maxArg then outStr = outStr..' '..(args['c'] or 'and') .. ' ' end
end
outStr = string.format(
'%s[[User:%s|%s]]',
outStr,
args[i],
args['label'..tostring(i)] or args[i]
)
end
end
outStr = outStr..(args['p'] or ':')
return mw.text.tag('span', {['class']='template-ping'}, outStr)
end
end
return p
14f0cd73a8a9f122c0e0e15382219083c602c62a
Module:Formatted appearance
828
89
171
170
2023-08-28T02:15:28Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
-- This module requires the use of Module:List.
local p = {}
-- Local function which is used to get a correctly formatted entry.
-- Function checks if the array had a value added by checking the counter,
-- and returns the relevant result.
local function getFormattedEntry(args, counter)
if (counter == 1) then -- Check if the counter stayed the same.
return "" -- Nothing was added to array; Return empty string.
elseif (counter == 2) then -- Check if only one value was added to the array.
return args[1] -- Only one value was added to array; Return that value.
else -- The array had more than one value added.
return table.concat(args, "<br/>") -- Tetrieve the formatted plainlist.
end
end
--[[
Local function which is used to format an appearance for a comic book,
in the style of:
Line 1: <comic book title> #<issue number> (with comic book title in italics)
Line 2: <release date>
For other usages, see createGenericEntry().
The function works with the following combinations:
-- Only comic book title (example: "The Incredible Hulk").
-- Title and issue number (example: "The Incredible Hulk" and "181").
-- Title and release date (example: "The Incredible Hulk and "November 1974").
-- Title, issue number and release date (example: "The Incredible Hulk", "181" and "November 1974").
-- Only release date (example: "November 1974").
--]]
local function createComicEntry(appearanceMajor, appearanceMinor, appearanceDate)
local fullString = {} -- Variable to save the array.
local counter = 1 -- Variable to save the array counter.
if (appearanceMajor ~= nil) then -- Check if a comic book title was entered.
if (appearanceMinor == nil) then -- A comic book title was entered; Check if a issue number was entered.
fullString[counter] = appearanceMajor -- A issue was not entered; Add only the comic book title to the array.
counter = counter + 1 -- Increment counter by one.
else
fullString[counter] = appearanceMajor .. " " .. appearanceMinor -- A issue was entered; Add both to the array.
counter = counter + 1 -- Increment counter by one.
end
end
if (appearanceDate ~= nil) then -- Check if a release date was entered.
fullString[counter] = appearanceDate -- A release date was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry.
end
--[[
Local function which is used to format an appearance for most usages,
including television, film, books, songs and games, in the style of:
Line 1: <minor work title> (in quotes) (Minor works include: TV episodes, chapters, songs and game missions)
Line 2: <major work title> (in italics) (Major works include: TV series, films, books, albums and games)
Line 3: <release date>
For comic book usages, see createComicEntry().
The function works with the following combinations:
-- Only minor work title (example: "Live Together, Die Alone").
-- Minor work title and major work title (example: "Live Together, Die Alone" and "Lost").
-- Minor work title and release date (example: "Live Together, Die Alone" and "May 24, 2006").
-- Minor work title, major work title and release date (example: "Live Together, Die Alone", "Lost" and "May 24, 2006").
-- Only major work title (example: "Lost").
-- major work title and release date (example: "Lost" and "May 24, 2006").
-- Only release date (example: "May 24, 2006").
--]]
local function createGenericEntry(appearanceMajor, appearanceMinor, appearanceDate)
local fullString = {} -- Variable to save the array.
local counter = 1 -- Variable to save the array counter.
if (appearanceMinor ~= nil) then -- Check if a minor appearance was entered.
fullString[counter] = appearanceMinor -- A minor appearance was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
if (appearanceMajor ~= nil) then -- Check if a major appearance was entered.
fullString[counter] = appearanceMajor -- A major appearance was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
if (appearanceDate ~= nil) then -- Check if a release date was entered.
fullString[counter] = appearanceDate -- A release date was entered; Add it to the array.
counter = counter + 1 -- Increment counter by one.
end
return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry.
end
-- Local function which is used to format with a hash symbol comic book issues.
-- For other minor works, see getFormattedGenericMinorWork().
local function getFormattedComicMinorWorkTitle(issue)
if (issue ~= nil) then -- Check if the issue is not nil.
if (string.find(issue, "#")) then -- Check if the issue already has a hash symbol.
return issue -- Hash symbol already present; Return issue.
else
local formattedString = string.gsub(issue, "%d+", "#%1") -- Hash symbol not found; Add the symbol before the issue number.
return formattedString -- Return issue.
end
else
return nil -- issue is nil; Return nil.
end
end
-- Local function which is used to format with quotes a minor work title of most types.
-- For comic book issues, see getFormattedComicMinorWork() (see [MOS:MINORWORK]).
local function getFormattedGenericMinorWorkTitle(title)
if (title ~= nil) then -- Check if the title is not nil.
return "\"" .. title .. "\"" -- Title is not nil; Add quotes to the title.
else
return nil -- Title is nil; Return nil.
end
end
-- Local function which is used to format with italics a major work title (see [MOS:MAJORWORK]).
local function getFormattedMajorWorkTitle(title)
if (title ~= nil) then -- Check if the title is not nil.
return "''" .. title .. "''" -- Title is not nil; Add italics to the title.
else
return nil -- Title is nil; Return nil.
end
end
-- Local function which does the actual main process.
local function _getFormattedAppearance(args)
local appearanceMajor = args['major_work'] -- Get the title of the major work.
local appearanceMinor = args['minor_work'] -- Get the title of the minor work.
local isComic = false -- Variable to save the status of wether the appearence is from a comic book.
if (args['issue'] ~= nil) then -- Check if the comic specific issue is not nil.
appearanceMinor = args['issue'] -- Issue is not nil; Get the issue number.
isComic = true -- Set isComic to true.
end
local appearanceDate = args['date'] -- Get the release date of the minor work.
local formattedAppearanceMajor = getFormattedMajorWorkTitle(appearanceMajor) -- Call getFormattedMajorWorkTitle() to get a formatted major work title.
if (isComic == false) then -- Check if the appearance is a comic book appearance.
-- The appearance is not a comic book appearance;
local formattedAppearanceMinor = getFormattedGenericMinorWorkTitle(appearanceMinor) -- Call getFormattedGenericMinorWorkTitle() to get a formatted minor work title.
return createGenericEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createGenericEntry() to create an appearance entry.
else
-- The appearance is a comic book appearance.
local formattedAppearanceMinor = getFormattedComicMinorWorkTitle(appearanceMinor) -- Call getFormattedComicMinorWorkTitle() to get a formatted minor work title.
return createComicEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createComicEntry() to create a comic book appearance entry.
end
end
--[[
Public function which is used to format the |first_appeared= and |last_appeared= fields.
The usage of this module allows for correct title formatting (see [MOS:MAJORWORK] and [MOS:MINORWORK]),
and correct line breaks based on guidelines (see [WP:UBLIST]).
Parameters:
-- |major_work= — optional; The title of the major work the fictional element appeared in.
Major works include TV series, films, books, albums and games.
-- |minor_work= — optional; The title of the minor work the fictional element appeared in.
Minor works include TV episodes, chapters, songs and game missions.
-- |issue= — optional; The number of the comic book issue the fictional element appeared in.
-- |date= — optional; The date of the publication/release of the minor work where the fictional element appeared in.
--]]
function p.getFormattedAppearance(frame)
local getArgs = require('Module:Arguments').getArgs -- Use Module:Arguments to access module arguments.
local args = getArgs(frame) -- Get the arguments sent via the template.
return _getFormattedAppearance(args) -- Call _getFormattedAppearance() to perform the actual process.
end
return p
983d4add2379f19ec30241c0470bf9b6c4089eb2
Module:Message box
828
90
173
172
2023-08-28T02:15:29Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
-- This is a meta-module for producing message box templates, including
-- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
-- Load necessary modules.
require('Module:No globals')
local getArgs
local yesno = require('Module:Yesno')
local templatestyles = 'Module:Message box/styles.css'
-- Get a language object for formatDate and ucfirst.
local lang = mw.language.getContentLanguage()
-- Define constants
local CONFIG_MODULE = 'Module:Message box/configuration'
local DEMOSPACES = {user = 'tmbox', talk = 'tmbox', image = 'imbox', file = 'imbox', category = 'cmbox', article = 'ambox', main = 'ambox'}
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function getTitleObject(...)
-- Get the title object, passing the function through pcall
-- in case we are over the expensive function count limit.
local success, title = pcall(mw.title.new, ...)
if success then
return title
end
end
local function union(t1, t2)
-- Returns the union of two arrays.
local vals = {}
for i, v in ipairs(t1) do
vals[v] = true
end
for i, v in ipairs(t2) do
vals[v] = true
end
local ret = {}
for k in pairs(vals) do
table.insert(ret, k)
end
table.sort(ret)
return ret
end
local function getArgNums(args, prefix)
local nums = {}
for k, v in pairs(args) do
local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
if num then
table.insert(nums, tonumber(num))
end
end
table.sort(nums)
return nums
end
--------------------------------------------------------------------------------
-- Box class definition
--------------------------------------------------------------------------------
local MessageBox = {}
MessageBox.__index = MessageBox
function MessageBox.new(boxType, args, cfg)
args = args or {}
local obj = {}
-- Set the title object and the namespace.
obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle()
-- Set the config for our box type.
obj.cfg = cfg[boxType]
if not obj.cfg then
local ns = obj.title.namespace
-- boxType is "mbox" or invalid input
if args.demospace and args.demospace ~= '' then
-- implement demospace parameter of mbox
local demospace = string.lower(args.demospace)
if DEMOSPACES[demospace] then
-- use template from DEMOSPACES
obj.cfg = cfg[DEMOSPACES[demospace]]
elseif string.find( demospace, 'talk' ) then
-- demo as a talk page
obj.cfg = cfg.tmbox
else
-- default to ombox
obj.cfg = cfg.ombox
end
elseif ns == 0 then
obj.cfg = cfg.ambox -- main namespace
elseif ns == 6 then
obj.cfg = cfg.imbox -- file namespace
elseif ns == 14 then
obj.cfg = cfg.cmbox -- category namespace
else
local nsTable = mw.site.namespaces[ns]
if nsTable and nsTable.isTalk then
obj.cfg = cfg.tmbox -- any talk namespace
else
obj.cfg = cfg.ombox -- other namespaces or invalid input
end
end
end
-- Set the arguments, and remove all blank arguments except for the ones
-- listed in cfg.allowBlankParams.
do
local newArgs = {}
for k, v in pairs(args) do
if v ~= '' then
newArgs[k] = v
end
end
for i, param in ipairs(obj.cfg.allowBlankParams or {}) do
newArgs[param] = args[param]
end
obj.args = newArgs
end
-- Define internal data structure.
obj.categories = {}
obj.classes = {}
-- For lazy loading of [[Module:Category handler]].
obj.hasCategories = false
return setmetatable(obj, MessageBox)
end
function MessageBox:addCat(ns, cat, sort)
if not cat then
return nil
end
if sort then
cat = string.format('[[Category:%s|%s]]', cat, sort)
else
cat = string.format('[[Category:%s]]', cat)
end
self.hasCategories = true
self.categories[ns] = self.categories[ns] or {}
table.insert(self.categories[ns], cat)
end
function MessageBox:addClass(class)
if not class then
return nil
end
table.insert(self.classes, class)
end
function MessageBox:setParameters()
local args = self.args
local cfg = self.cfg
-- Get type data.
self.type = args.type
local typeData = cfg.types[self.type]
self.invalidTypeError = cfg.showInvalidTypeError
and self.type
and not typeData
typeData = typeData or cfg.types[cfg.default]
self.typeClass = typeData.class
self.typeImage = typeData.image
-- Find whether we are using a small message box.
self.isSmall = cfg.allowSmall and (
cfg.smallParam and args.small == cfg.smallParam
or not cfg.smallParam and yesno(args.small)
)
-- Add attributes, classes and styles.
self.id = args.id
self.name = args.name
if self.name then
self:addClass('box-' .. string.gsub(self.name,' ','_'))
end
if yesno(args.plainlinks) ~= false then
self:addClass('plainlinks')
end
for _, class in ipairs(cfg.classes or {}) do
self:addClass(class)
end
if self.isSmall then
self:addClass(cfg.smallClass or 'mbox-small')
end
self:addClass(self.typeClass)
self:addClass(args.class)
self.style = args.style
self.attrs = args.attrs
-- Set text style.
self.textstyle = args.textstyle
-- Find if we are on the template page or not. This functionality is only
-- used if useCollapsibleTextFields is set, or if both cfg.templateCategory
-- and cfg.templateCategoryRequireName are set.
self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
if self.useCollapsibleTextFields
or cfg.templateCategory
and cfg.templateCategoryRequireName
then
if self.name then
local templateName = mw.ustring.match(
self.name,
'^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$'
) or self.name
templateName = 'Template:' .. templateName
self.templateTitle = getTitleObject(templateName)
end
self.isTemplatePage = self.templateTitle
and mw.title.equals(self.title, self.templateTitle)
end
-- Process data for collapsible text fields. At the moment these are only
-- used in {{ambox}}.
if self.useCollapsibleTextFields then
-- Get the self.issue value.
if self.isSmall and args.smalltext then
self.issue = args.smalltext
else
local sect
if args.sect == '' then
sect = 'This ' .. (cfg.sectionDefault or 'page')
elseif type(args.sect) == 'string' then
sect = 'This ' .. args.sect
end
local issue = args.issue
issue = type(issue) == 'string' and issue ~= '' and issue or nil
local text = args.text
text = type(text) == 'string' and text or nil
local issues = {}
table.insert(issues, sect)
table.insert(issues, issue)
table.insert(issues, text)
self.issue = table.concat(issues, ' ')
end
-- Get the self.talk value.
local talk = args.talk
-- Show talk links on the template page or template subpages if the talk
-- parameter is blank.
if talk == ''
and self.templateTitle
and (
mw.title.equals(self.templateTitle, self.title)
or self.title:isSubpageOf(self.templateTitle)
)
then
talk = '#'
elseif talk == '' then
talk = nil
end
if talk then
-- If the talk value is a talk page, make a link to that page. Else
-- assume that it's a section heading, and make a link to the talk
-- page of the current page with that section heading.
local talkTitle = getTitleObject(talk)
local talkArgIsTalkPage = true
if not talkTitle or not talkTitle.isTalkPage then
talkArgIsTalkPage = false
talkTitle = getTitleObject(
self.title.text,
mw.site.namespaces[self.title.namespace].talk.id
)
end
if talkTitle and talkTitle.exists then
local talkText = 'Relevant discussion may be found on'
if talkArgIsTalkPage then
talkText = string.format(
'%s [[%s|%s]].',
talkText,
talk,
talkTitle.prefixedText
)
else
talkText = string.format(
'%s the [[%s#%s|talk page]].',
talkText,
talkTitle.prefixedText,
talk
)
end
self.talk = talkText
end
end
-- Get other values.
self.fix = args.fix ~= '' and args.fix or nil
local date
if args.date and args.date ~= '' then
date = args.date
elseif args.date == '' and self.isTemplatePage then
date = lang:formatDate('F Y')
end
if date then
self.date = string.format(" <small class='date-container'>''(<span class='date'>%s</span>)''</small>", date)
end
self.info = args.info
end
-- Set the non-collapsible text field. At the moment this is used by all box
-- types other than ambox, and also by ambox when small=yes.
if self.isSmall then
self.text = args.smalltext or args.text
else
self.text = args.text
end
-- Set the below row.
self.below = cfg.below and args.below
-- General image settings.
self.imageCellDiv = not self.isSmall and cfg.imageCellDiv
self.imageEmptyCell = cfg.imageEmptyCell
if cfg.imageEmptyCellStyle then
self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
end
-- Left image settings.
local imageLeft = self.isSmall and args.smallimage or args.image
if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
or not cfg.imageCheckBlank and imageLeft ~= 'none'
then
self.imageLeft = imageLeft
if not imageLeft then
local imageSize = self.isSmall
and (cfg.imageSmallSize or '30x30px')
or '40x40px'
self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage
or 'Imbox notice.png', imageSize)
end
end
-- Right image settings.
local imageRight = self.isSmall and args.smallimageright or args.imageright
if not (cfg.imageRightNone and imageRight == 'none') then
self.imageRight = imageRight
end
end
function MessageBox:setMainspaceCategories()
local args = self.args
local cfg = self.cfg
if not cfg.allowMainspaceCategories then
return nil
end
local nums = {}
for _, prefix in ipairs{'cat', 'category', 'all'} do
args[prefix .. '1'] = args[prefix]
nums = union(nums, getArgNums(args, prefix))
end
-- The following is roughly equivalent to the old {{Ambox/category}}.
local date = args.date
date = type(date) == 'string' and date
local preposition = 'from'
for _, num in ipairs(nums) do
local mainCat = args['cat' .. tostring(num)]
or args['category' .. tostring(num)]
local allCat = args['all' .. tostring(num)]
mainCat = type(mainCat) == 'string' and mainCat
allCat = type(allCat) == 'string' and allCat
if mainCat and date and date ~= '' then
local catTitle = string.format('%s %s %s', mainCat, preposition, date)
self:addCat(0, catTitle)
catTitle = getTitleObject('Category:' .. catTitle)
if not catTitle or not catTitle.exists then
self:addCat(0, 'Articles with invalid date parameter in template')
end
elseif mainCat and (not date or date == '') then
self:addCat(0, mainCat)
end
if allCat then
self:addCat(0, allCat)
end
end
end
function MessageBox:setTemplateCategories()
local args = self.args
local cfg = self.cfg
-- Add template categories.
if cfg.templateCategory then
if cfg.templateCategoryRequireName then
if self.isTemplatePage then
self:addCat(10, cfg.templateCategory)
end
elseif not self.title.isSubpage then
self:addCat(10, cfg.templateCategory)
end
end
-- Add template error categories.
if cfg.templateErrorCategory then
local templateErrorCategory = cfg.templateErrorCategory
local templateCat, templateSort
if not self.name and not self.title.isSubpage then
templateCat = templateErrorCategory
elseif self.isTemplatePage then
local paramsToCheck = cfg.templateErrorParamsToCheck or {}
local count = 0
for i, param in ipairs(paramsToCheck) do
if not args[param] then
count = count + 1
end
end
if count > 0 then
templateCat = templateErrorCategory
templateSort = tostring(count)
end
if self.categoryNums and #self.categoryNums > 0 then
templateCat = templateErrorCategory
templateSort = 'C'
end
end
self:addCat(10, templateCat, templateSort)
end
end
function MessageBox:setAllNamespaceCategories()
-- Set categories for all namespaces.
if self.invalidTypeError then
local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText
self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
end
end
function MessageBox:setCategories()
if self.title.namespace == 0 then
self:setMainspaceCategories()
elseif self.title.namespace == 10 then
self:setTemplateCategories()
end
self:setAllNamespaceCategories()
end
function MessageBox:renderCategories()
if not self.hasCategories then
-- No categories added, no need to pass them to Category handler so,
-- if it was invoked, it would return the empty string.
-- So we shortcut and return the empty string.
return ""
end
-- Convert category tables to strings and pass them through
-- [[Module:Category handler]].
return require('Module:Category handler')._main{
main = table.concat(self.categories[0] or {}),
template = table.concat(self.categories[10] or {}),
all = table.concat(self.categories.all or {}),
nocat = self.args.nocat,
page = self.args.page
}
end
function MessageBox:export()
local root = mw.html.create()
-- Create the box table.
local boxTable = root:tag('table')
boxTable:attr('id', self.id or nil)
for i, class in ipairs(self.classes or {}) do
boxTable:addClass(class or nil)
end
boxTable
:cssText(self.style or nil)
:attr('role', 'presentation')
if self.attrs then
boxTable:attr(self.attrs)
end
-- Add the left-hand image.
local row = boxTable:tag('tr')
if self.imageLeft then
local imageLeftCell = row:tag('td'):addClass('mbox-image')
if self.imageCellDiv then
-- If we are using a div, redefine imageLeftCell so that the image
-- is inside it. Divs use style="width: 52px;", which limits the
-- image width to 52px. If any images in a div are wider than that,
-- they may overlap with the text or cause other display problems.
imageLeftCell = imageLeftCell:tag('div'):css('width', '52px')
end
imageLeftCell:wikitext(self.imageLeft or nil)
elseif self.imageEmptyCell then
-- Some message boxes define an empty cell if no image is specified, and
-- some don't. The old template code in templates where empty cells are
-- specified gives the following hint: "No image. Cell with some width
-- or padding necessary for text cell to have 100% width."
row:tag('td')
:addClass('mbox-empty-cell')
:cssText(self.imageEmptyCellStyle or nil)
end
-- Add the text.
local textCell = row:tag('td'):addClass('mbox-text')
if self.useCollapsibleTextFields then
-- The message box uses advanced text parameters that allow things to be
-- collapsible. At the moment, only ambox uses this.
textCell:cssText(self.textstyle or nil)
local textCellDiv = textCell:tag('div')
textCellDiv
:addClass('mbox-text-span')
:wikitext(self.issue or nil)
if (self.talk or self.fix) and not self.isSmall then
textCellDiv:tag('span')
:addClass('hide-when-compact')
:wikitext(self.talk and (' ' .. self.talk) or nil)
:wikitext(self.fix and (' ' .. self.fix) or nil)
end
textCellDiv:wikitext(self.date and (' ' .. self.date) or nil)
if self.info and not self.isSmall then
textCellDiv
:tag('span')
:addClass('hide-when-compact')
:wikitext(self.info and (' ' .. self.info) or nil)
end
else
-- Default text formatting - anything goes.
textCell
:cssText(self.textstyle or nil)
:wikitext(self.text or nil)
end
-- Add the right-hand image.
if self.imageRight then
local imageRightCell = row:tag('td'):addClass('mbox-imageright')
if self.imageCellDiv then
-- If we are using a div, redefine imageRightCell so that the image
-- is inside it.
imageRightCell = imageRightCell:tag('div'):css('width', '52px')
end
imageRightCell
:wikitext(self.imageRight or nil)
end
-- Add the below row.
if self.below then
boxTable:tag('tr')
:tag('td')
:attr('colspan', self.imageRight and '3' or '2')
:addClass('mbox-text')
:cssText(self.textstyle or nil)
:wikitext(self.below or nil)
end
-- Add error message for invalid type parameters.
if self.invalidTypeError then
root:tag('div')
:css('text-align', 'center')
:wikitext(string.format(
'This message box is using an invalid "type=%s" parameter and needs fixing.',
self.type or ''
))
end
-- Add categories.
root:wikitext(self:renderCategories() or nil)
return tostring(root)
end
--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------
local p, mt = {}, {}
function p._exportClasses()
-- For testing.
return {
MessageBox = MessageBox
}
end
function p.main(boxType, args, cfgTables)
local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE))
box:setParameters()
box:setCategories()
return box:export()
end
local function templatestyles(frame, src)
return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
.. 'CONFIG_MODULE'
end
function mt.__index(t, k)
return function (frame)
if not getArgs then
getArgs = require('Module:Arguments').getArgs
end
return t.main(k, getArgs(frame, {trim = false, removeBlanks = false}))
end
end
return setmetatable(p, mt)
be00cd389f9f2afcd361e5d5e33622839555cbd9
Module:Message box/configuration
828
91
175
174
2023-08-28T02:15:29Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
--------------------------------------------------------------------------------
-- Message box configuration --
-- --
-- This module contains configuration data for [[Module:Message box]]. --
--------------------------------------------------------------------------------
return {
ambox = {
types = {
speedy = {
class = 'ambox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'ambox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'ambox-content',
image = 'Ambox important.svg'
},
style = {
class = 'ambox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'ambox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'ambox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'ambox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
allowBlankParams = {'talk', 'sect', 'date', 'issue', 'fix', 'hidden'},
allowSmall = true,
smallParam = 'left',
smallClass = 'mbox-small-left',
classes = {'metadata', 'ambox'},
imageEmptyCell = true,
imageCheckBlank = true,
imageSmallSize = '20x20px',
imageCellDiv = true,
useCollapsibleTextFields = true,
imageRightNone = true,
sectionDefault = 'article',
allowMainspaceCategories = true,
templateCategory = 'Article message templates',
templateCategoryRequireName = true,
templateErrorCategory = 'Article message templates with missing parameters',
templateErrorParamsToCheck = {'issue', 'fix'},
},
cmbox = {
types = {
speedy = {
class = 'cmbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'cmbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'cmbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'cmbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'cmbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'cmbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'cmbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'cmbox'},
imageEmptyCell = true
},
fmbox = {
types = {
warning = {
class = 'fmbox-warning',
image = 'Ambox warning pn.svg'
},
editnotice = {
class = 'fmbox-editnotice',
image = 'Information icon4.svg'
},
system = {
class = 'fmbox-system',
image = 'Information icon4.svg'
}
},
default = 'system',
showInvalidTypeError = true,
classes = {'fmbox'},
imageEmptyCell = false,
imageRightNone = false
},
imbox = {
types = {
speedy = {
class = 'imbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'imbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'imbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'imbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'imbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'imbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
license = {
class = 'imbox-license licensetpl',
image = 'Imbox license.png' -- @todo We need an SVG version of this
},
featured = {
class = 'imbox-featured',
image = 'Cscr-featured.svg'
},
notice = {
class = 'imbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'imbox'},
imageEmptyCell = true,
below = true,
templateCategory = 'File message boxes'
},
ombox = {
types = {
speedy = {
class = 'ombox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'ombox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'ombox-content',
image = 'Ambox important.svg'
},
style = {
class = 'ombox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'ombox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'ombox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'ombox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'ombox'},
allowSmall = true,
imageEmptyCell = true,
imageRightNone = true
},
tmbox = {
types = {
speedy = {
class = 'tmbox-speedy',
image = 'Ambox warning pn.svg'
},
delete = {
class = 'tmbox-delete',
image = 'Ambox warning pn.svg'
},
content = {
class = 'tmbox-content',
image = 'Ambox important.svg'
},
style = {
class = 'tmbox-style',
image = 'Edit-clear.svg'
},
move = {
class = 'tmbox-move',
image = 'Merge-split-transwiki default.svg'
},
protection = {
class = 'tmbox-protection',
image = 'Semi-protection-shackle-keyhole.svg'
},
notice = {
class = 'tmbox-notice',
image = 'Information icon4.svg'
}
},
default = 'notice',
showInvalidTypeError = true,
classes = {'tmbox'},
allowSmall = true,
imageRightNone = true,
imageEmptyCell = true,
imageEmptyCellStyle = true,
templateCategory = 'Talk message boxes'
}
}
c6bd9191861b23e474e12b19c694335c4bc3af5f
Module:TNT
828
92
177
176
2023-08-28T02:15:29Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
--
-- INTRO: (!!! DO NOT RENAME THIS PAGE !!!)
-- This module allows any template or module to be copy/pasted between
-- wikis without any translation changes. All translation text is stored
-- in the global Data:*.tab pages on Commons, and used everywhere.
--
-- SEE: https://www.mediawiki.org/wiki/Multilingual_Templates_and_Modules
--
-- ATTENTION:
-- Please do NOT rename this module - it has to be identical on all wikis.
-- This code is maintained at https://www.mediawiki.org/wiki/Module:TNT
-- Please do not modify it anywhere else, as it may get copied and override your changes.
-- Suggestions can be made at https://www.mediawiki.org/wiki/Module_talk:TNT
--
-- DESCRIPTION:
-- The "msg" function uses a Commons dataset to translate a message
-- with a given key (e.g. source-table), plus optional arguments
-- to the wiki markup in the current content language.
-- Use lang=xx to set language. Example:
--
-- {{#invoke:TNT | msg
-- | I18n/Template:Graphs.tab <!-- https://commons.wikimedia.org/wiki/Data:I18n/Template:Graphs.tab -->
-- | source-table <!-- uses a translation message with id = "source-table" -->
-- | param1 }} <!-- optional parameter -->
--
--
-- The "doc" function will generate the <templatedata> parameter documentation for templates.
-- This way all template parameters can be stored and localized in a single Commons dataset.
-- NOTE: "doc" assumes that all documentation is located in Data:Templatedata/* on Commons.
--
-- {{#invoke:TNT | doc | Graph:Lines }}
-- uses https://commons.wikimedia.org/wiki/Data:Templatedata/Graph:Lines.tab
-- if the current page is Template:Graph:Lines/doc
--
local p = {}
local i18nDataset = 'I18n/Module:TNT.tab'
-- Forward declaration of the local functions
local sanitizeDataset, loadData, link, formatMessage
function p.msg(frame)
local dataset, id
local params = {}
local lang = nil
for k, v in pairs(frame.args) do
if k == 1 then
dataset = mw.text.trim(v)
elseif k == 2 then
id = mw.text.trim(v)
elseif type(k) == 'number' then
params[k - 2] = mw.text.trim(v)
elseif k == 'lang' and v ~= '_' then
lang = mw.text.trim(v)
end
end
return formatMessage(dataset, id, params, lang)
end
-- Identical to p.msg() above, but used from other lua modules
-- Parameters: name of dataset, message key, optional arguments
-- Example with 2 params: format('I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset')
function p.format(dataset, key, ...)
local checkType = require('libraryUtil').checkType
checkType('format', 1, dataset, 'string')
checkType('format', 2, key, 'string')
return formatMessage(dataset, key, {...})
end
-- Identical to p.msg() above, but used from other lua modules with the language param
-- Parameters: language code, name of dataset, message key, optional arguments
-- Example with 2 params: formatInLanguage('es', I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset')
function p.formatInLanguage(lang, dataset, key, ...)
local checkType = require('libraryUtil').checkType
checkType('formatInLanguage', 1, lang, 'string')
checkType('formatInLanguage', 2, dataset, 'string')
checkType('formatInLanguage', 3, key, 'string')
return formatMessage(dataset, key, {...}, lang)
end
-- Obsolete function that adds a 'c:' prefix to the first param.
-- "Sandbox/Sample.tab" -> 'c:Data:Sandbox/Sample.tab'
function p.link(frame)
return link(frame.args[1])
end
function p.doc(frame)
local dataset = 'Templatedata/' .. sanitizeDataset(frame.args[1])
return frame:extensionTag('templatedata', p.getTemplateData(dataset)) ..
formatMessage(i18nDataset, 'edit_doc', {link(dataset)})
end
function p.getTemplateData(dataset)
-- TODO: add '_' parameter once lua starts reindexing properly for "all" languages
local data = loadData(dataset)
local names = {}
for _, field in ipairs(data.schema.fields) do
table.insert(names, field.name)
end
local params = {}
local paramOrder = {}
for _, row in ipairs(data.data) do
local newVal = {}
local name = nil
for pos, columnName in ipairs(names) do
if columnName == 'name' then
name = row[pos]
else
newVal[columnName] = row[pos]
end
end
if name then
params[name] = newVal
table.insert(paramOrder, name)
end
end
-- Work around json encoding treating {"1":{...}} as an [{...}]
params['zzz123']=''
local json = mw.text.jsonEncode({
params=params,
paramOrder=paramOrder,
description=data.description
})
json = string.gsub(json,'"zzz123":"",?', "")
return json
end
-- Local functions
sanitizeDataset = function(dataset)
if not dataset then
return nil
end
dataset = mw.text.trim(dataset)
if dataset == '' then
return nil
elseif string.sub(dataset,-4) ~= '.tab' then
return dataset .. '.tab'
else
return dataset
end
end
loadData = function(dataset, lang)
dataset = sanitizeDataset(dataset)
if not dataset then
error(formatMessage(i18nDataset, 'error_no_dataset', {}))
end
-- Give helpful error to thirdparties who try and copy this module.
if not mw.ext or not mw.ext.data or not mw.ext.data.get then
error(string.format([['''Missing JsonConfig extension, or not properly configured;
Cannot load https://commons.wikimedia.org/wiki/Data:%s. Please properly enable the JSONConfig extension at Special:ManageWiki/extensions#mw-prefsection-other
See https://www.mediawiki.org/wiki/Extension:JsonConfig#Supporting_Wikimedia_templates''']], dataset))
end
local data = mw.ext.data.get(dataset, lang)
if data == false then
if dataset == i18nDataset then
-- Prevent cyclical calls
error('Missing Commons dataset ' .. i18nDataset)
else
error(formatMessage(i18nDataset, 'error_bad_dataset', {link(dataset)}))
end
end
return data
end
-- Given a dataset name, convert it to a title with the 'commons:data:' prefix
link = function(dataset)
return 'c:Data:' .. mw.text.trim(dataset or '')
end
formatMessage = function(dataset, key, params, lang)
for _, row in pairs(loadData(dataset, lang).data) do
local id, msg = unpack(row)
if id == key then
local result = mw.message.newRawMessage(msg, unpack(params or {}))
return result:plain()
end
end
if dataset == i18nDataset then
-- Prevent cyclical calls
error('Invalid message key "' .. key .. '"')
else
error(formatMessage(i18nDataset, 'error_bad_msgkey', {key, link(dataset)}))
end
end
return p
6d981852d69d5958a60d96d24c311680564c6103
Module:String
828
93
179
178
2023-08-28T02:15:30Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
--[[
This module is intended to provide access to basic string functions.
Most of the functions provided here can be invoked with named parameters,
unnamed parameters, or a mixture. If named parameters are used, Mediawiki will
automatically remove any leading or trailing whitespace from the parameter.
Depending on the intended use, it may be advantageous to either preserve or
remove such whitespace.
Global options
ignore_errors: If set to 'true' or 1, any error condition will result in
an empty string being returned rather than an error message.
error_category: If an error occurs, specifies the name of a category to
include with the error message. The default category is
[Category:Errors reported by Module String].
no_category: If set to 'true' or 1, no category will be added if an error
is generated.
Unit tests for this module are available at Module:String/tests.
]]
local str = {}
--[[
len
This function returns the length of the target string.
Usage:
{{#invoke:String|len|target_string|}}
OR
{{#invoke:String|len|s=target_string}}
Parameters
s: The string whose length to report
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the target string.
]]
function str.len( frame )
local new_args = str._getParameters( frame.args, {'s'} )
local s = new_args['s'] or ''
return mw.ustring.len( s )
end
--[[
sub
This function returns a substring of the target string at specified indices.
Usage:
{{#invoke:String|sub|target_string|start_index|end_index}}
OR
{{#invoke:String|sub|s=target_string|i=start_index|j=end_index}}
Parameters
s: The string to return a subset of
i: The fist index of the substring to return, defaults to 1.
j: The last index of the string to return, defaults to the last character.
The first character of the string is assigned an index of 1. If either i or j
is a negative value, it is interpreted the same as selecting a character by
counting from the end of the string. Hence, a value of -1 is the same as
selecting the last character of the string.
If the requested indices are out of range for the given string, an error is
reported.
]]
function str.sub( frame )
local new_args = str._getParameters( frame.args, { 's', 'i', 'j' } )
local s = new_args['s'] or ''
local i = tonumber( new_args['i'] ) or 1
local j = tonumber( new_args['j'] ) or -1
local len = mw.ustring.len( s )
-- Convert negatives for range checking
if i < 0 then
i = len + i + 1
end
if j < 0 then
j = len + j + 1
end
if i > len or j > len or i < 1 or j < 1 then
return str._error( 'String subset index out of range' )
end
if j < i then
return str._error( 'String subset indices out of order' )
end
return mw.ustring.sub( s, i, j )
end
--[[
_match
This function returns a substring from the source string that matches a
specified pattern. It is exported for use in other modules
Usage:
strmatch = require("Module:String")._match
sresult = strmatch( s, pattern, start, match, plain, nomatch )
Parameters
s: The string to search
pattern: The pattern or string to find within the string
start: The index within the source string to start the search. The first
character of the string has index 1. Defaults to 1.
match: In some cases it may be possible to make multiple matches on a single
string. This specifies which match to return, where the first match is
match= 1. If a negative number is specified then a match is returned
counting from the last match. Hence match = -1 is the same as requesting
the last match. Defaults to 1.
plain: A flag indicating that the pattern should be understood as plain
text. Defaults to false.
nomatch: If no match is found, output the "nomatch" value rather than an error.
For information on constructing Lua patterns, a form of [regular expression], see:
* http://www.lua.org/manual/5.1/manual.html#5.4.1
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns
]]
-- This sub-routine is exported for use in other modules
function str._match( s, pattern, start, match_index, plain_flag, nomatch )
if s == '' then
return str._error( 'Target string is empty' )
end
if pattern == '' then
return str._error( 'Pattern string is empty' )
end
start = tonumber(start) or 1
if math.abs(start) < 1 or math.abs(start) > mw.ustring.len( s ) then
return str._error( 'Requested start is out of range' )
end
if match_index == 0 then
return str._error( 'Match index is out of range' )
end
if plain_flag then
pattern = str._escapePattern( pattern )
end
local result
if match_index == 1 then
-- Find first match is simple case
result = mw.ustring.match( s, pattern, start )
else
if start > 1 then
s = mw.ustring.sub( s, start )
end
local iterator = mw.ustring.gmatch(s, pattern)
if match_index > 0 then
-- Forward search
for w in iterator do
match_index = match_index - 1
if match_index == 0 then
result = w
break
end
end
else
-- Reverse search
local result_table = {}
local count = 1
for w in iterator do
result_table[count] = w
count = count + 1
end
result = result_table[ count + match_index ]
end
end
if result == nil then
if nomatch == nil then
return str._error( 'Match not found' )
else
return nomatch
end
else
return result
end
end
--[[
match
This function returns a substring from the source string that matches a
specified pattern.
Usage:
{{#invoke:String|match|source_string|pattern_string|start_index|match_number|plain_flag|nomatch_output}}
OR
{{#invoke:String|match|s=source_string|pattern=pattern_string|start=start_index
|match=match_number|plain=plain_flag|nomatch=nomatch_output}}
Parameters
s: The string to search
pattern: The pattern or string to find within the string
start: The index within the source string to start the search. The first
character of the string has index 1. Defaults to 1.
match: In some cases it may be possible to make multiple matches on a single
string. This specifies which match to return, where the first match is
match= 1. If a negative number is specified then a match is returned
counting from the last match. Hence match = -1 is the same as requesting
the last match. Defaults to 1.
plain: A flag indicating that the pattern should be understood as plain
text. Defaults to false.
nomatch: If no match is found, output the "nomatch" value rather than an error.
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from each string. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
If the match_number or start_index are out of range for the string being queried, then
this function generates an error. An error is also generated if no match is found.
If one adds the parameter ignore_errors=true, then the error will be suppressed and
an empty string will be returned on any failure.
For information on constructing Lua patterns, a form of [regular expression], see:
* http://www.lua.org/manual/5.1/manual.html#5.4.1
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
* http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns
]]
-- This is the entry point for #invoke:String|match
function str.match( frame )
local new_args = str._getParameters( frame.args, {'s', 'pattern', 'start', 'match', 'plain', 'nomatch'} )
local s = new_args['s'] or ''
local start = tonumber( new_args['start'] ) or 1
local plain_flag = str._getBoolean( new_args['plain'] or false )
local pattern = new_args['pattern'] or ''
local match_index = math.floor( tonumber(new_args['match']) or 1 )
local nomatch = new_args['nomatch']
return str._match( s, pattern, start, match_index, plain_flag, nomatch )
end
--[[
pos
This function returns a single character from the target string at position pos.
Usage:
{{#invoke:String|pos|target_string|index_value}}
OR
{{#invoke:String|pos|target=target_string|pos=index_value}}
Parameters
target: The string to search
pos: The index for the character to return
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the target string. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
The first character has an index value of 1.
If one requests a negative value, this function will select a character by counting backwards
from the end of the string. In other words pos = -1 is the same as asking for the last character.
A requested value of zero, or a value greater than the length of the string returns an error.
]]
function str.pos( frame )
local new_args = str._getParameters( frame.args, {'target', 'pos'} )
local target_str = new_args['target'] or ''
local pos = tonumber( new_args['pos'] ) or 0
if pos == 0 or math.abs(pos) > mw.ustring.len( target_str ) then
return str._error( 'String index out of range' )
end
return mw.ustring.sub( target_str, pos, pos )
end
--[[
find
This function allows one to search for a target string or pattern within another
string.
Usage:
{{#invoke:String|find|source_str|target_string|start_index|plain_flag}}
OR
{{#invoke:String|find|source=source_str|target=target_str|start=start_index|plain=plain_flag}}
Parameters
source: The string to search
target: The string or pattern to find within source
start: The index within the source string to start the search, defaults to 1
plain: Boolean flag indicating that target should be understood as plain
text and not as a Lua style regular expression, defaults to true
If invoked using named parameters, Mediawiki will automatically remove any leading or
trailing whitespace from the parameter. In some circumstances this is desirable, in
other cases one may want to preserve the whitespace.
This function returns the first index >= "start" where "target" can be found
within "source". Indices are 1-based. If "target" is not found, then this
function returns 0. If either "source" or "target" are missing / empty, this
function also returns 0.
This function should be safe for UTF-8 strings.
]]
function str.find( frame )
local new_args = str._getParameters( frame.args, {'source', 'target', 'start', 'plain' } )
local source_str = new_args['source'] or ''
local pattern = new_args['target'] or ''
local start_pos = tonumber(new_args['start']) or 1
local plain = new_args['plain'] or true
if source_str == '' or pattern == '' then
return 0
end
plain = str._getBoolean( plain )
local start = mw.ustring.find( source_str, pattern, start_pos, plain )
if start == nil then
start = 0
end
return start
end
--[[
replace
This function allows one to replace a target string or pattern within another
string.
Usage:
{{#invoke:String|replace|source_str|pattern_string|replace_string|replacement_count|plain_flag}}
OR
{{#invoke:String|replace|source=source_string|pattern=pattern_string|replace=replace_string|
count=replacement_count|plain=plain_flag}}
Parameters
source: The string to search
pattern: The string or pattern to find within source
replace: The replacement text
count: The number of occurences to replace, defaults to all.
plain: Boolean flag indicating that pattern should be understood as plain
text and not as a Lua style regular expression, defaults to true
]]
function str.replace( frame )
local new_args = str._getParameters( frame.args, {'source', 'pattern', 'replace', 'count', 'plain' } )
local source_str = new_args['source'] or ''
local pattern = new_args['pattern'] or ''
local replace = new_args['replace'] or ''
local count = tonumber( new_args['count'] )
local plain = new_args['plain'] or true
if source_str == '' or pattern == '' then
return source_str
end
plain = str._getBoolean( plain )
if plain then
pattern = str._escapePattern( pattern )
replace = mw.ustring.gsub( replace, "%%", "%%%%" ) --Only need to escape replacement sequences.
end
local result
if count ~= nil then
result = mw.ustring.gsub( source_str, pattern, replace, count )
else
result = mw.ustring.gsub( source_str, pattern, replace )
end
return result
end
--[[
simple function to pipe string.rep to templates.
]]
function str.rep( frame )
local repetitions = tonumber( frame.args[2] )
if not repetitions then
return str._error( 'function rep expects a number as second parameter, received "' .. ( frame.args[2] or '' ) .. '"' )
end
return string.rep( frame.args[1] or '', repetitions )
end
--[[
escapePattern
This function escapes special characters from a Lua string pattern. See [1]
for details on how patterns work.
[1] https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns
Usage:
{{#invoke:String|escapePattern|pattern_string}}
Parameters
pattern_string: The pattern string to escape.
]]
function str.escapePattern( frame )
local pattern_str = frame.args[1]
if not pattern_str then
return str._error( 'No pattern string specified' )
end
local result = str._escapePattern( pattern_str )
return result
end
--[[
count
This function counts the number of occurrences of one string in another.
]]
function str.count(frame)
local args = str._getParameters(frame.args, {'source', 'pattern', 'plain'})
local source = args.source or ''
local pattern = args.pattern or ''
local plain = str._getBoolean(args.plain or true)
if plain then
pattern = str._escapePattern(pattern)
end
local _, count = mw.ustring.gsub(source, pattern, '')
return count
end
--[[
endswith
This function determines whether a string ends with another string.
]]
function str.endswith(frame)
local args = str._getParameters(frame.args, {'source', 'pattern'})
local source = args.source or ''
local pattern = args.pattern or ''
if pattern == '' then
-- All strings end with the empty string.
return "yes"
end
if mw.ustring.sub(source, -mw.ustring.len(pattern), -1) == pattern then
return "yes"
else
return ""
end
end
--[[
join
Join all non empty arguments together; the first argument is the separator.
Usage:
{{#invoke:String|join|sep|one|two|three}}
]]
function str.join(frame)
local args = {}
local sep
for _, v in ipairs( frame.args ) do
if sep then
if v ~= '' then
table.insert(args, v)
end
else
sep = v
end
end
return table.concat( args, sep or '' )
end
--[[
Helper function that populates the argument list given that user may need to use a mix of
named and unnamed parameters. This is relevant because named parameters are not
identical to unnamed parameters due to string trimming, and when dealing with strings
we sometimes want to either preserve or remove that whitespace depending on the application.
]]
function str._getParameters( frame_args, arg_list )
local new_args = {}
local index = 1
local value
for _, arg in ipairs( arg_list ) do
value = frame_args[arg]
if value == nil then
value = frame_args[index]
index = index + 1
end
new_args[arg] = value
end
return new_args
end
--[[
Helper function to handle error messages.
]]
function str._error( error_str )
local frame = mw.getCurrentFrame()
local error_category = frame.args.error_category or 'Errors reported by Module String'
local ignore_errors = frame.args.ignore_errors or false
local no_category = frame.args.no_category or false
if str._getBoolean(ignore_errors) then
return ''
end
local error_str = '<strong class="error">String Module Error: ' .. error_str .. '</strong>'
if error_category ~= '' and not str._getBoolean( no_category ) then
error_str = '[[Category:' .. error_category .. ']]' .. error_str
end
return error_str
end
--[[
Helper Function to interpret boolean strings
]]
function str._getBoolean( boolean_str )
local boolean_value
if type( boolean_str ) == 'string' then
boolean_str = boolean_str:lower()
if boolean_str == 'false' or boolean_str == 'no' or boolean_str == '0'
or boolean_str == '' then
boolean_value = false
else
boolean_value = true
end
elseif type( boolean_str ) == 'boolean' then
boolean_value = boolean_str
else
error( 'No boolean value found' )
end
return boolean_value
end
--[[
Helper function that escapes all pattern characters so that they will be treated
as plain text.
]]
function str._escapePattern( pattern_str )
return mw.ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" )
end
return str
73c9d229ca32cb5e05a3873238b69fec347cf4b1
Module:Userbox
828
94
181
180
2023-08-28T02:15:30Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
-- This module implements {{userbox}}.
local p = {}
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function checkNum(val, default)
-- Checks whether a value is a number greater than or equal to zero. If so,
-- returns it as a number. If not, returns a default value.
val = tonumber(val)
if val and val >= 0 then
return val
else
return default
end
end
local function addSuffix(num, suffix)
-- Turns a number into a string and adds a suffix.
if num then
return tostring(num) .. suffix
else
return nil
end
end
local function checkNumAndAddSuffix(num, default, suffix)
-- Checks a value with checkNum and adds a suffix.
num = checkNum(num, default)
return addSuffix(num, suffix)
end
local function makeCat(cat, sort)
-- Makes a category link.
if sort then
return mw.ustring.format('[[Category:%s|%s]]', cat, sort)
else
return mw.ustring.format('[[Category:%s]]', cat)
end
end
--------------------------------------------------------------------------------
-- Argument processing
--------------------------------------------------------------------------------
local function makeInvokeFunc(funcName)
return function (frame)
local origArgs = require('Module:Arguments').getArgs(frame)
local args = {}
for k, v in pairs(origArgs) do
args[k] = v
end
return p.main(funcName, args)
end
end
p.userbox = makeInvokeFunc('_userbox')
p['userbox-2'] = makeInvokeFunc('_userbox-2')
p['userbox-r'] = makeInvokeFunc('_userbox-r')
--------------------------------------------------------------------------------
-- Main functions
--------------------------------------------------------------------------------
function p.main(funcName, args)
local userboxData = p[funcName](args)
local userbox = p.render(userboxData)
local cats = p.categories(args)
return userbox .. (cats or '')
end
function p._userbox(args)
-- Does argument processing for {{userbox}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee'
-- Get info values.
data.info = args.info or args[4] or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt')
data.infoHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em'
data.infoColor = args['info-color'] or args['info-fc'] or 'black'
data.infoOtherParams = args['info-other-param'] or args['info-op']
data.infoClass = args['info-class']
-- Get id values.
local id = args.logo or args[3] or args.id
data.id = id
data.showId = id and true or false
data.idWidth = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px')
data.idHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.idBackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd'
data.idTextAlign = args['id-a'] or 'center'
data.idFontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt')
data.idColor = args['logo-color'] or args['id-fc'] or data.infoColor
data.idPadding = args['logo-padding'] or args['id-p'] or '0 1px 0 0'
data.idLineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em'
data.idOtherParams = args['logo-other-param'] or args['id-op']
data.idClass = args['id-class']
return data
end
p['_userbox-2'] = function (args)
-- Does argument processing for {{userbox-2}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args[9] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args[1] or args['border-c'] or args['id1-c'] or '#999999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args[2] or args['info-c'] or '#eeeeee'
-- Get info values.
data.info = args[4] or args.info or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-s'], 8, 'pt')
data.infoColor = args[8] or args['info-fc'] or 'black'
data.infoPadding = args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-lh'] or '1.25em'
data.infoOtherParams = args['info-op']
-- Get id values.
data.showId = true
data.id = args.logo or args[3] or args.id1 or 'id1'
data.idWidth = checkNumAndAddSuffix(args['id1-w'], 45, 'px')
data.idHeight = checkNumAndAddSuffix(args['id-h'], 45, 'px')
data.idBackgroundColor = args[1] or args['id1-c'] or '#dddddd'
data.idTextAlign = 'center'
data.idFontSize = checkNumAndAddSuffix(args['id1-s'], 14, 'pt')
data.idLineHeight = args['id1-lh'] or '1.25em'
data.idColor = args['id1-fc'] or data.infoColor
data.idPadding = args['id1-p'] or '0 1px 0 0'
data.idOtherParams = args['id1-op']
-- Get id2 values.
data.showId2 = true
data.id2 = args.logo or args[5] or args.id2 or 'id2'
data.id2Width = checkNumAndAddSuffix(args['id2-w'], 45, 'px')
data.id2Height = data.idHeight
data.id2BackgroundColor = args[7] or args['id2-c'] or args[1] or '#dddddd'
data.id2TextAlign = 'center'
data.id2FontSize = checkNumAndAddSuffix(args['id2-s'], 14, 'pt')
data.id2LineHeight = args['id2-lh'] or '1.25em'
data.id2Color = args['id2-fc'] or data.infoColor
data.id2Padding = args['id2-p'] or '0 0 0 1px'
data.id2OtherParams = args['id2-op']
return data
end
p['_userbox-r'] = function (args)
-- Does argument processing for {{userbox-r}}.
local data = {}
-- Get div tag values.
data.float = args.float or 'left'
local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width.
data.borderWidth = addSuffix(borderWidthNum, 'px')
data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999'
data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag.
data.bodyClass = args.bodyclass
-- Get table tag values.
data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee'
-- Get id values.
data.showId = false -- We only show id2 in userbox-r.
-- Get info values.
data.info = args.info or args[4] or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-align'] or args['info-a'] or 'left'
data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt')
data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px'
data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em'
data.infoColor = args['info-color'] or args['info-fc'] or 'black'
data.infoOtherParams = args['info-other-param'] or args['info-op']
-- Get id2 values.
data.showId2 = true
data.id2 = args.logo or args[3] or args.id or 'id'
data.id2Width = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px')
data.id2Height = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px')
data.id2BackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd'
data.id2TextAlign = args['id-a'] or 'center'
data.id2FontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt')
data.id2Color = args['logo-color'] or args['id-fc'] or data.infoColor
data.id2Padding = args['logo-padding'] or args['id-p'] or '0 0 0 1px'
data.id2LineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em'
data.id2OtherParams = args['logo-other-param'] or args['id-op']
return data
end
function p.render(data)
-- Renders the userbox html using the content of the data table.
-- Render the div tag html.
local root = mw.html.create('div')
root
:css('float', data.float)
:css('border', (data.borderWidth or '') .. ' solid ' .. (data.borderColor or ''))
:css('margin', '1px')
:css('width', data.width)
:addClass('wikipediauserbox')
:addClass(data.bodyClass)
-- Render the table tag html.
local tableroot = root:tag('table')
tableroot
:css('border-collapse', 'collapse')
:css('width', data.width)
:css('margin-bottom', '0')
:css('margin-top', '0')
:css('background', data.backgroundColor)
-- Render the id html.
local tablerow = tableroot:tag('tr')
if data.showId then
tablerow:tag('th')
:css('border', '0')
:css('width', data.idWidth)
:css('height', data.idHeight)
:css('background', data.idBackgroundColor)
:css('text-align', data.idTextAlign)
:css('font-size', data.idFontSize)
:css('color', data.idColor)
:css('padding', data.idPadding)
:css('line-height', data.idLineHeight)
:css('vertical-align', 'middle')
:cssText(data.idOtherParams)
:addClass(data.idClass)
:wikitext(data.id)
end
-- Render the info html.
tablerow:tag('td')
:css('border', '0')
:css('text-align', data.infoTextAlign)
:css('font-size', data.infoFontSize)
:css('padding', data.infoPadding)
:css('height', data.infoHeight)
:css('line-height', data.infoLineHeight)
:css('color', data.infoColor)
:css('vertical-align', 'middle')
:cssText(data.infoOtherParams)
:addClass(data.infoClass)
:wikitext(data.info)
-- Render the second id html.
if data.showId2 then
tablerow:tag('th')
:css('border', '0')
:css('width', data.id2Width)
:css('height', data.id2Height)
:css('background', data.id2BackgroundColor)
:css('text-align', data.id2TextAlign)
:css('font-size', data.id2FontSize)
:css('color', data.id2Color)
:css('padding', data.id2Padding)
:css('line-height', data.id2LineHeight)
:css('vertical-align', 'middle')
:cssText(data.id2OtherParams)
:wikitext(data.id2)
end
local title = mw.title.getCurrentTitle()
if (title.namespace == 2) and not title.text:match("/") then
return tostring(root) -- regular user page
elseif title.namespace == 14 then
return tostring(root) -- category
elseif title.isTalkPage then
return tostring(root) -- talk page
end
local function has_text(wikitext)
local function get_alt(text)
return text:match("|alt=([^|]*)") or ""
end
wikitext = wikitext:gsub("]]", "|]]")
wikitext = wikitext:gsub("%[%[%s*[Mm][Ee][Dd][Ii][Aa]%s*:[^|]-(|.-)]]", get_alt)
wikitext = wikitext:gsub("%[%[%s*[Ii][Mm][Aa][Gg][Ee]%s*:[^|]-(|.-)]]", get_alt)
wikitext = wikitext:gsub("%[%[%s*[Ff][Ii][Ll][Ee]%s*:[^|]-(|.-)]]", get_alt)
return mw.text.trim(wikitext) ~= ""
end
return tostring(root)
end
function p.categories(args, page)
-- Gets categories
-- The page parameter makes the function act as though the module was being called from that page.
-- It is included for testing purposes.
local cats = {}
cats[#cats + 1] = args.usercategory
cats[#cats + 1] = args.usercategory2
cats[#cats + 1] = args.usercategory3
if #cats > 0 and not require("Module:Yesno")(args.nocat) then
-- Get the title object
local title
if page then
title = mw.title.new(page)
else
title = mw.title.getCurrentTitle()
end
-- Build category handler arguments.
local chargs = {}
chargs.page = page
chargs.main = '[[Category:Pages with misplaced templates]]'
if title.namespace == 2 then
-- User namespace.
local user = ''
for i, cat in ipairs(cats) do
user = user .. makeCat(cat)
end
return user
elseif title.namespace == 10 then
-- Template namespace.
local basepage = title.baseText
local template = ''
for i, cat in ipairs(cats) do
template = template .. makeCat(cat, ' ' .. basepage)
end
return template
end
end
end
return p
aac333efff739f0243d8ffced6f4296cffb8d7e9
Module:Yesno
828
95
183
182
2023-08-28T02:15:30Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0
Scribunto
text/plain
-- Function allowing for consistent treatment of boolean-like wikitext input.
-- It works similarly to the template {{yesno}}.
return function (val, default)
-- If your wiki uses non-ascii characters for any of "yes", "no", etc., you
-- should replace "val:lower()" with "mw.ustring.lower(val)" in the
-- following line.
val = type(val) == 'string' and val:lower() or val
if val == nil then
return nil
elseif val == true
or val == 'yes'
or val == 'y'
or val == 'true'
or val == 't'
or val == 'on'
or tonumber(val) == 1
then
return true
elseif val == false
or val == 'no'
or val == 'n'
or val == 'false'
or val == 'f'
or val == 'off'
or tonumber(val) == 0
then
return false
else
return default
end
end
f767643e7d12126d020d88d662a3dd057817b9dc
File:Logo.svg
6
96
184
2023-08-28T02:27:29Z
Alxira5
4
Tux the Penguipedia for your wiki and social networks.
wikitext
text/x-wiki
== Summary ==
Tux the Penguipedia for your wiki and social networks.
63a5089ad64bfa6e59c6e6715d0e1e494fa80b7d
Module:Documentation/styles.css
828
97
186
185
2023-08-29T02:59:04Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
text
text/plain
.documentation,
.documentation-metadata {
border: 1px solid #a2a9b1;
background-color: #ecfcf4;
clear: both;
}
.documentation {
margin: 1em 0 0 0;
padding: 1em;
}
.documentation-metadata {
margin: 0.2em 0; /* same margin left-right as .documentation */
font-style: italic;
padding: 0.4em 1em; /* same padding left-right as .documentation */
}
.documentation-startbox {
padding-bottom: 3px;
border-bottom: 1px solid #aaa;
margin-bottom: 1ex;
}
.documentation-heading {
font-weight: bold;
font-size: 125%;
}
.documentation-clear { /* Don't want things to stick out where they shouldn't. */
clear: both;
}
.documentation-toolbar {
font-style: normal;
font-size: 85%;
}
/* [[Category:Template stylesheets]] */
5fb984fe8632dc068db16853a824c9f3d5175dd9
Template:If empty
10
98
188
187
2023-08-29T02:59:04Z
Alxira5
4
1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license
wikitext
text/x-wiki
<includeonly>{{{{{|safesubst:}}}#if:{{{1|}}}
| {{{1}}}
| {{{{{|safesubst:}}}#if:{{{2|}}}
| {{{2}}}
| {{{{{|safesubst:}}}#if:{{{3|}}}
| {{{3}}}
| {{{{{|safesubst:}}}#if:{{{4|}}}
| {{{4}}}
| {{{{{|safesubst:}}}#if:{{{5|}}}
| {{{5}}}
| {{{{{|safesubst:}}}#if:{{{6|}}}
| {{{6}}}
| {{{{{|safesubst:}}}#if:{{{7|}}}
| {{{7}}}
| {{{{{|safesubst:}}}#if:{{{8|}}}
| {{{8}}}
| {{{{{|safesubst:}}}#if:{{{9|}}}
| {{{9}}}
}}
}}
}}
}}
}}
}}
}}
}}
}}</includeonly><noinclude>
{{Documentation}}
<!-- Add categories and interwikis to the /doc subpage, not here! -->
</noinclude>
eeda2c13231e9a8b44d480e8c429d73652575009
Tux the Penguipedia:Code of conduct
4
99
189
2023-08-29T04:30:22Z
Alxira5
4
Create the encyclopedia code of conduct page
wikitext
text/x-wiki
{{header
| title = Code of conduct
| notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content.
| bodyhex = 1accf0
}}
== Contributor Covenant Code of Conduct ==
=== Our Pledge ===
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
=== Our Standards ===
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others’ private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
=== Enforcement Responsibilities ===
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
=== Scope ===
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
=== Enforcement ===
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
=== Enforcement Guidelines ===
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
==== 1. Correction ====
'''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
'''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
==== 2. Warning ====
'''Community Impact''': A violation through a single incident or series of actions.
'''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
==== 3. Temporary Ban ====
'''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior.
'''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
==== 4. Permanent Ban ====
'''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
'''Consequence''': A permanent ban from any sort of public interaction within the community.
=== Attribution ===
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder].
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations.
[[Category:Documentation]]
715ac70e161fa41223bae3bebab0e32611c8095e
190
189
2023-08-29T04:33:19Z
Alxira5
4
Protected "[[Tux the Penguipedia:Code of conduct]]": [object Object]Protect this page to avoid vandalism ([Edit=Allow only administrators] (indefinite))
wikitext
text/x-wiki
{{header
| title = Code of conduct
| notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content.
| bodyhex = 1accf0
}}
== Contributor Covenant Code of Conduct ==
=== Our Pledge ===
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
=== Our Standards ===
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others’ private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
=== Enforcement Responsibilities ===
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
=== Scope ===
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
=== Enforcement ===
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
=== Enforcement Guidelines ===
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
==== 1. Correction ====
'''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
'''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
==== 2. Warning ====
'''Community Impact''': A violation through a single incident or series of actions.
'''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
==== 3. Temporary Ban ====
'''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior.
'''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
==== 4. Permanent Ban ====
'''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
'''Consequence''': A permanent ban from any sort of public interaction within the community.
=== Attribution ===
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder].
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations.
[[Category:Documentation]]
715ac70e161fa41223bae3bebab0e32611c8095e
193
190
2023-08-29T16:31:41Z
Alxira5
4
I added the color of the top bar
wikitext
text/x-wiki
{{header
| title = Code of conduct
| notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content.
| topbarhex = 118da6
| bodyhex = 1accf0
}}
== Contributor Covenant Code of Conduct ==
=== Our Pledge ===
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
=== Our Standards ===
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others’ private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
=== Enforcement Responsibilities ===
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
=== Scope ===
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
=== Enforcement ===
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
=== Enforcement Guidelines ===
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
==== 1. Correction ====
'''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
'''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
==== 2. Warning ====
'''Community Impact''': A violation through a single incident or series of actions.
'''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
==== 3. Temporary Ban ====
'''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior.
'''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
==== 4. Permanent Ban ====
'''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
'''Consequence''': A permanent ban from any sort of public interaction within the community.
=== Attribution ===
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder].
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations.
[[Category:Documentation]]
80f036d60555d6331b91da13f66787a724c59292
194
193
2023-08-29T23:28:33Z
Alxira5
4
Fixed the contact link and changed the header color to make it more visible in Citizen's dark theme
wikitext
text/x-wiki
{{header
| title = Code of conduct
| notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content.
}}
== Contributor Covenant Code of Conduct ==
=== Our Pledge ===
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
=== Our Standards ===
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others’ private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
=== Enforcement Responsibilities ===
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
=== Scope ===
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
=== Enforcement ===
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [[User talk:Alxira5|Alxira5]]. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
=== Enforcement Guidelines ===
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
==== 1. Correction ====
'''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
'''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
==== 2. Warning ====
'''Community Impact''': A violation through a single incident or series of actions.
'''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
==== 3. Temporary Ban ====
'''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior.
'''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
==== 4. Permanent Ban ====
'''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
'''Consequence''': A permanent ban from any sort of public interaction within the community.
=== Attribution ===
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder].
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations.
[[Category:Documentation]]
af539569987c56eca9337631582d71f50d58ed7a
Category:Documentation
14
100
191
2023-08-29T05:50:19Z
Alxira5
4
Add the information about the category
wikitext
text/x-wiki
In this category are the articles of the Tux the Penguipedia documentation.
For more information, visit [[Tux the Penguipedia:Documentation]].
1aa14f06044a1e76206247bddd32707d8c855aaf
192
191
2023-08-29T05:51:56Z
Alxira5
4
Protected "[[Category:Documentation]]": nullProtect this category to avoid vandalism ([Edit=Allow only administrators] (indefinite))
wikitext
text/x-wiki
In this category are the articles of the Tux the Penguipedia documentation.
For more information, visit [[Tux the Penguipedia:Documentation]].
1aa14f06044a1e76206247bddd32707d8c855aaf
MediaWiki:Citizen-footer-tagline
8
101
195
2023-08-29T23:37:08Z
Alxira5
4
Create encyclopedia social links
wikitext
text/x-wiki
[https://mastodon.online/@TuxthePenguipedia {{#fab:mastodon}}] [irc://irc.libera.chat/#tuxthepenguipedia {{#fas:comment}}]
e9e2054bee8d789e508023c365fe34b4741470ed
User:AlXGX21
2
14
196
23
2023-11-16T01:48:46Z
Void
5
Void moved page [[User:AlexGX22]] to [[User:AlXGX21]]: Automatically moved page while renaming the user "[[Special:CentralAuth/AlexGX22|AlexGX22]]" to "[[Special:CentralAuth/AlXGX21|AlXGX21]]"
wikitext
text/x-wiki
#REDIRECT [[User:Alxira5|</nowiki>Alxira5]]
5cd0368e4dea8043dc1782a4946317e31e03d2a2
User:AlexGX22
2
102
197
2023-11-16T01:48:52Z
Void
5
Void moved page [[User:AlexGX22]] to [[User:AlXGX21]]: Automatically moved page while renaming the user "[[Special:CentralAuth/AlexGX22|AlexGX22]]" to "[[Special:CentralAuth/AlXGX21|AlXGX21]]"
wikitext
text/x-wiki
#REDIRECT [[User:AlXGX21]]
a5424424e8c4d8ed9e35757f04b9f769735a5abe